[Python-checkins] cpython: Implement PEP 451 (ModuleSpec).

eric.snow python-checkins at python.org
Fri Nov 22 17:17:12 CET 2013


http://hg.python.org/cpython/rev/07229c6104b1
changeset:   87347:07229c6104b1
user:        Eric Snow <ericsnowcurrently at gmail.com>
date:        Fri Nov 22 09:05:39 2013 -0700
summary:
  Implement PEP 451 (ModuleSpec).

files:
  Doc/library/importlib.rst                                  |   144 +-
  Doc/reference/import.rst                                   |   429 +-
  Doc/whatsnew/3.4.rst                                       |    20 +
  Lib/imp.py                                                 |    30 +-
  Lib/importlib/__init__.py                                  |    64 +-
  Lib/importlib/_bootstrap.py                                |  1107 +-
  Lib/importlib/abc.py                                       |   102 +-
  Lib/importlib/machinery.py                                 |     1 +
  Lib/importlib/util.py                                      |    65 +-
  Lib/multiprocessing/spawn.py                               |    11 +-
  Lib/pkgutil.py                                             |    11 +-
  Lib/pydoc.py                                               |     3 +-
  Lib/test/test_descr.py                                     |     2 +-
  Lib/test/test_frozen.py                                    |    77 -
  Lib/test/test_import.py                                    |    14 +-
  Lib/test/test_importlib/abc.py                             |     5 -
  Lib/test/test_importlib/builtin/test_finder.py             |    59 +-
  Lib/test/test_importlib/builtin/test_loader.py             |    78 +-
  Lib/test/test_importlib/extension/test_case_sensitivity.py |     2 +
  Lib/test/test_importlib/extension/test_finder.py           |    20 +-
  Lib/test/test_importlib/extension/test_loader.py           |    81 +-
  Lib/test/test_importlib/frozen/test_finder.py              |    45 +-
  Lib/test/test_importlib/frozen/test_loader.py              |    79 +-
  Lib/test/test_importlib/import_/test_meta_path.py          |     4 +-
  Lib/test/test_importlib/test_abc.py                        |   197 -
  Lib/test/test_importlib/test_api.py                        |   136 +-
  Lib/test/test_importlib/test_spec.py                       |   968 +
  Lib/test/test_importlib/test_util.py                       |    65 -
  Lib/test/test_module.py                                    |    12 +-
  Lib/test/test_namespace_pkgs.py                            |    20 -
  Lib/test/test_pkg.py                                       |    20 +-
  Lib/test/test_pkgutil.py                                   |    16 +-
  Lib/test/test_reprlib.py                                   |     1 +
  Lib/test/test_runpy.py                                     |     5 +-
  Objects/moduleobject.c                                     |    53 +-
  Python/import.c                                            |    16 +-
  Python/importlib.h                                         |  7415 +++++----
  37 files changed, 6981 insertions(+), 4396 deletions(-)


diff --git a/Doc/library/importlib.rst b/Doc/library/importlib.rst
--- a/Doc/library/importlib.rst
+++ b/Doc/library/importlib.rst
@@ -310,11 +310,11 @@
         from the import. If the loader inserted a module and the load fails, it
         must be removed by the loader from :data:`sys.modules`; modules already
         in :data:`sys.modules` before the loader began execution should be left
-        alone (see :func:`importlib.util.module_to_load`).
+        alone (see :func:`importlib.util.module_for_loader`).
 
         The loader should set several attributes on the module.
         (Note that some of these attributes can change when a module is
-        reloaded; see :meth:`init_module_attrs`):
+        reloaded):
 
         - :attr:`__name__`
             The name of the module.
@@ -357,17 +357,6 @@
         .. versionchanged:: 3.4
            Made optional instead of an abstractmethod.
 
-    .. method:: init_module_attrs(module)
-
-        Set the :attr:`__loader__` attribute on the module.
-
-        Subclasses overriding this method should set whatever appropriate
-        attributes it can, getting the module's name from :attr:`__name__` when
-        needed. All values should also be overridden so that reloading works as
-        expected.
-
-        .. versionadded:: 3.4
-
 
 .. class:: ResourceLoader
 
@@ -442,14 +431,6 @@
 
         .. versionadded:: 3.4
 
-    .. method:: init_module_attrs(module)
-
-        Set the :attr:`__package__` attribute and :attr:`__path__` attribute to
-        the empty list if appropriate along with what
-        :meth:`importlib.abc.Loader.init_module_attrs` sets.
-
-        .. versionadded:: 3.4
-
     .. method:: load_module(fullname)
 
         Implementation of :meth:`Loader.load_module`.
@@ -474,15 +455,6 @@
         .. versionchanged:: 3.4
            Raises :exc:`ImportError` instead of :exc:`NotImplementedError`.
 
-    .. method:: init_module_attrs(module)
-
-        Set :attr:`__file__` and if initializing a package then set
-        :attr:`__path__` to ``[os.path.dirname(__file__)]`` along with
-        all attributes set by
-        :meth:`importlib.abc.InspectLoader.init_module_attrs`.
-
-        .. versionadded:: 3.4
-
 
 .. class:: FileLoader(fullname, path)
 
@@ -599,14 +571,6 @@
         ``__init__`` when the file extension is removed **and** the module name
         itself does not end in ``__init__``.
 
-    .. method:: init_module_attr(module)
-
-        Set :attr:`__cached__` using :func:`imp.cache_from_source`. Other
-        attributes set by
-        :meth:`importlib.abc.ExecutionLoader.init_module_attrs`.
-
-        .. versionadded:: 3.4
-
 
 :mod:`importlib.machinery` -- Importers and path hooks
 ------------------------------------------------------
@@ -882,6 +846,64 @@
       .. versionadded:: 3.4
 
 
+.. class:: ModuleSpec(name, loader, *, origin=None, loader_state=None, is_package=None)
+
+   A specification for a module's import-system-related state.
+
+   .. versionadded:: 3.4
+
+   .. attribute:: name
+
+   (``__name__``)
+
+   A string for the fully-qualified name of the module.
+
+   .. attribute:: loader
+
+   (``__loader__``)
+
+   The loader to use for loading.  For namespace packages this should be
+   set to None.
+
+   .. attribute:: origin
+
+   (``__file__``)
+
+   Name of the place from which the module is loaded, e.g. "builtin" for
+   built-in modules and the filename for modules loaded from source.
+   Normally "origin" should be set, but it may be None (the default)
+   which indicates it is unspecified.
+
+   .. attribute:: submodule_search_locations
+
+   (``__path__``)
+
+   List of strings for where to find submodules, if a package (None
+   otherwise).
+
+   .. attribute:: loader_state
+
+   Container of extra module-specific data for use during loading (or
+   None).
+
+   .. attribute:: cached
+
+   (``__cached__``)
+
+   String for where the compiled module should be stored (or None).
+
+   .. attribute:: parent
+
+   (``__package__``)
+
+   (Read-only) Fully-qualified name of the package to which the module
+   belongs as a submodule (or None).
+
+   .. attribute:: has_location
+
+   (Read-only) Boolean indicating whether or not the module's "origin"
+   attribute refers to a loadable location.
+
 :mod:`importlib.util` -- Utility code for importers
 ---------------------------------------------------
 
@@ -952,20 +974,6 @@
 
    .. versionadded:: 3.3
 
-.. function:: module_to_load(name, *, reset_name=True)
-
-    Returns a :term:`context manager` which provides the module to load. The
-    module will either come from :attr:`sys.modules` in the case of reloading or
-    a fresh module if loading a new module. Proper cleanup of
-    :attr:`sys.modules` occurs if the module was new and an exception was
-    raised.
-
-    If **reset_name** is true and the module requested is being reloaded then
-    the module's :attr:`__name__` attribute will
-    be reset to **name**, else it will be left untouched.
-
-    .. versionadded:: 3.4
-
 .. decorator:: module_for_loader
 
     A :term:`decorator` for :meth:`importlib.abc.Loader.load_module`
@@ -999,9 +1007,8 @@
        unconditionally to support reloading.
 
     .. deprecated:: 3.4
-        For the benefit of :term:`loader` subclasses, please use
-        :func:`module_to_load` and
-        :meth:`importlib.abc.Loader.init_module_attrs` instead.
+       The import machinery now directly performs all the functionality
+       provided by this function.
 
 .. decorator:: set_loader
 
@@ -1012,11 +1019,6 @@
    the wrapped method (i.e. ``self``) is what :attr:`__loader__` should be set
    to.
 
-   .. note::
-      As this decorator sets :attr:`__loader__` after loading the module, it is
-      recommended to use :meth:`importlib.abc.Loader.init_module_attrs` instead
-      when appropriate.
-
    .. versionchanged:: 3.4
       Set ``__loader__`` if set to ``None``, as if the attribute does not
       exist.
@@ -1026,7 +1028,21 @@
    A :term:`decorator` for :meth:`importlib.abc.Loader.load_module` to set the :attr:`__package__` attribute on the returned module. If :attr:`__package__`
    is set and has a value other than ``None`` it will not be changed.
 
-   .. note::
-      As this decorator sets :attr:`__package__` after loading the module, it is
-      recommended to use :meth:`importlib.abc.Loader.init_module_attrs` instead
-      when appropriate.
+.. function:: spec_from_loader(name, loader, *, origin=None, is_package=None)
+
+   A factory function for creating a :class:`ModuleSpec` instance based
+   on a loader.  The parameters have the same meaning as they do for
+   ModuleSpec.  The function uses available :term:`loader` APIs, such as
+   :meth:`InspectLoader.is_package`, to fill in any missing
+   information on the spec.
+
+   .. versionadded:: 3.4
+
+.. function:: spec_from_file_location(name, location, *, loader=None, submodule_search_locations=None)
+
+   A factory function for creating a :class:`ModuleSpec` instance based
+   on the path to a file.  Missing information will be filled in on the
+   spec by making use of loader APIs and by the implication that the
+   module will be file-based.
+
+   .. versionadded:: 3.4
diff --git a/Doc/reference/import.rst b/Doc/reference/import.rst
--- a/Doc/reference/import.rst
+++ b/Doc/reference/import.rst
@@ -210,6 +210,7 @@
 .. index::
     single: finder
     single: loader
+    single: module spec
 
 If the named module is not found in :data:`sys.modules`, then Python's import
 protocol is invoked to find and load the module.  This protocol consists of
@@ -230,13 +231,17 @@
 range and scope of module searching.
 
 Finders do not actually load modules.  If they can find the named module, they
-return a :term:`loader`, which the import machinery then invokes to load the
-module and create the corresponding module object.
+return a :term:`module spec`, an encapsulation of the module's import-related
+information, which the import machinery then uses when loading the module.
 
 The following sections describe the protocol for finders and loaders in more
 detail, including how you can create and register new ones to extend the
 import machinery.
 
+.. versionchanged:: 3.4
+   In previous versions of Python, finders returned :term:`loaders <loader>`
+   directly, whereas now they return module specs which *contain* loaders.
+   Loaders are still used during import but have fewer responsibilities.
 
 Import hooks
 ------------
@@ -270,24 +275,23 @@
 
 .. index::
     single: sys.meta_path
-    pair: finder; find_module
-    pair: finder; find_loader
+    pair: finder; find_spec
 
 When the named module is not found in :data:`sys.modules`, Python next
 searches :data:`sys.meta_path`, which contains a list of meta path finder
 objects.  These finders are queried in order to see if they know how to handle
 the named module.  Meta path finders must implement a method called
-:meth:`find_module()` which takes two arguments, a name and an import path.
+:meth:`find_spec()` which takes two arguments, a name and an import path.
 The meta path finder can use any strategy it wants to determine whether it can
 handle the named module or not.
 
 If the meta path finder knows how to handle the named module, it returns a
-loader object.  If it cannot handle the named module, it returns ``None``.  If
+spec object.  If it cannot handle the named module, it returns ``None``.  If
 :data:`sys.meta_path` processing reaches the end of its list without returning
-a loader, then an :exc:`ImportError` is raised.  Any other exceptions raised
+a spec, then an :exc:`ImportError` is raised.  Any other exceptions raised
 are simply propagated up, aborting the import process.
 
-The :meth:`find_module()` method of meta path finders is called with two
+The :meth:`find_spec()` method of meta path finders is called with two
 arguments.  The first is the fully qualified name of the module being
 imported, for example ``foo.bar.baz``.  The second argument is the path
 entries to use for the module search.  For top-level modules, the second
@@ -299,12 +303,12 @@
 The meta path may be traversed multiple times for a single import request.
 For example, assuming none of the modules involved has already been cached,
 importing ``foo.bar.baz`` will first perform a top level import, calling
-``mpf.find_module("foo", None)`` on each meta path finder (``mpf``). After
+``mpf.find_spec("foo", None)`` on each meta path finder (``mpf``). After
 ``foo`` has been imported, ``foo.bar`` will be imported by traversing the
 meta path a second time, calling
-``mpf.find_module("foo.bar", foo.__path__)``. Once ``foo.bar`` has been
+``mpf.find_spec("foo.bar", foo.__path__)``. Once ``foo.bar`` has been
 imported, the final traversal will call
-``mpf.find_module("foo.bar.baz", foo.bar.__path__)``.
+``mpf.find_spec("foo.bar.baz", foo.bar.__path__)``.
 
 Some meta path finders only support top level imports. These importers will
 always return ``None`` when anything other than ``None`` is passed as the
@@ -315,131 +319,229 @@
 modules, and one that knows how to import modules from an :term:`import path`
 (i.e. the :term:`path based finder`).
 
+.. versionchanged:: 3.4
+   The find_spec() method of meta path finders replaced :meth:`find_module()`.
+   which is now deprecated.  While it will continue to work without change,
+   the import machinery will try it only if the finder does not implement
+   find_spec().
 
-Loaders
+
+Loading
 =======
 
-If and when a module loader is found its
-:meth:`~importlib.abc.Loader.load_module` method is called, with a single
-argument, the fully qualified name of the module being imported.  This method
-has several responsibilities, and should return the module object it has
-loaded [#fnlo]_.  If it cannot load the module, it should raise an
-:exc:`ImportError`, although any other exception raised during
-:meth:`load_module()` will be propagated.
+If and when a module spec is found, the import machinery will use it (and
+the loader it contains) when loading the module.  Here is an approximation
+of what happens during the loading portion of import::
 
-In many cases, the finder and loader can be the same object; in such cases the
-:meth:`finder.find_module()` would just return ``self``.
+    module = None
+    if spec.loader is not None and hasattr(spec.loader, 'create_module'):
+        module = spec.loader.create_module(spec)
+    if module is None:
+        module = ModuleType(spec.name)
+    # The import-related module attributes get set here:
+    _init_module_attrs(spec, module)
 
-Loaders must satisfy the following requirements:
+    if spec.loader is None:
+        if spec.submodule_search_locations is not None:
+            # namespace package
+            sys.modules[spec.name] = module
+        else:
+            # unsupported
+            raise ImportError
+    elif not hasattr(spec.loader, 'exec_module'):
+        module = spec.loader.load_module(spec.name)
+    else:
+        sys.modules[spec.name] = module
+        try:
+            spec.loader.exec_module(module)
+        except BaseException:
+            try:
+                del sys.modules[spec.name]
+            except KeyError:
+                pass
+            raise
+    module_to_return = sys.modules[spec.name]
+
+Note the following details:
 
  * If there is an existing module object with the given name in
-   :data:`sys.modules`, the loader must use that existing module.  (Otherwise,
-   :func:`imp.reload` will not work correctly.)  If the named module does
-   not exist in :data:`sys.modules`, the loader must create a new module
-   object and add it to :data:`sys.modules`.
+   :data:`sys.modules`, import will have already returned it.
 
-   Note that the module *must* exist in :data:`sys.modules` before the loader
+ * The module will exist in :data:`sys.modules` before the loader
    executes the module code.  This is crucial because the module code may
    (directly or indirectly) import itself; adding it to :data:`sys.modules`
    beforehand prevents unbounded recursion in the worst case and multiple
    loading in the best.
 
-   If loading fails, the loader must remove any modules it has inserted into
-   :data:`sys.modules`, but it must remove **only** the failing module, and
-   only if the loader itself has loaded it explicitly.  Any module already in
-   the :data:`sys.modules` cache, and any module that was successfully loaded
-   as a side-effect, must remain in the cache.
+ * If loading fails, the failing module -- and only the failing module --
+   gets removed from :data:`sys.modules`.  Any module already in the
+   :data:`sys.modules` cache, and any module that was successfully loaded
+   as a side-effect, must remain in the cache.  This contrasts with
+   reloading where even the failing module is left in :data:`sys.modules`.
 
- * The loader may set the ``__file__`` attribute of the module.  If set, this
-   attribute's value must be a string.  The loader may opt to leave
-   ``__file__`` unset if it has no semantic meaning (e.g. a module loaded from
-   a database). If ``__file__`` is set, it may also be appropriate to set the
-   ``__cached__`` attribute which is the path to any compiled version of the
-   code (e.g. byte-compiled file). The file does not need to exist to set this
-   attribute; the path can simply point to whether the compiled file would
-   exist (see :pep:`3147`).
+ * After the module is created but before execution, the import machinery
+   sets the import-related module attributes ("init_module_attrs"), as
+   summarized in a `later section <Import-related module attributes>`_.
 
- * The loader may set the ``__name__`` attribute of the module.  While not
-   required, setting this attribute is highly recommended so that the
-   :meth:`repr()` of the module is more informative.
+ * Module execution is the key moment of loading in which the module's
+   namespace gets populated.  Execution is entirely delegated to the
+   loader, which gets to decide what gets populated and how.
 
- * If the module is a package (either regular or namespace), the loader must
-   set the module object's ``__path__`` attribute.  The value must be
-   iterable, but may be empty if ``__path__`` has no further significance
-   to the loader. If ``__path__`` is not empty, it must produce strings
-   when iterated over. More details on the semantics of ``__path__`` are
-   given :ref:`below <package-path-rules>`.
+ * The module created during loading and passed to exec_module() may
+   not be the one returned at the end of import [#fnlo]_.
 
- * The ``__loader__`` attribute must be set to the loader object that loaded
-   the module.  This is mostly for introspection and reloading, but can be
-   used for additional loader-specific functionality, for example getting
-   data associated with a loader. If the attribute is missing or set to ``None``
-   then the import machinery will automatically set it **after** the module has
-   been imported.
+.. versionchanged:: 3.4
+   The import system has taken over the boilerplate responsibilities of
+   loaders.  These were previously performed by the :meth:`load_module()`
+   method.
 
- * The module's ``__package__`` attribute must be set.  Its value must be a
-   string, but it can be the same value as its ``__name__``.  If the attribute
-   is set to ``None`` or is missing, the import system will fill it in with a
-   more appropriate value **after** the module has been imported.
-   When the module is a package, its ``__package__`` value should be set to its
-   ``__name__``.  When the module is not a package, ``__package__`` should be
-   set to the empty string for top-level modules, or for submodules, to the
-   parent package's name.  See :pep:`366` for further details.
+Loaders
+-------
 
-   This attribute is used instead of ``__name__`` to calculate explicit
-   relative imports for main modules, as defined in :pep:`366`.
+Module loaders provide the critical function of loading: module execution.
+The import machinery calls the :meth:`~importlib.abc.Loader.exec_module()`
+method with a single argument, the module object to execute.  Any value
+returned from exec_module() is ignored.
+
+Loaders must satisfy the following requirements:
 
  * If the module is a Python module (as opposed to a built-in module or a
    dynamically loaded extension), the loader should execute the module's code
    in the module's global name space (``module.__dict__``).
 
+ * If loader cannot execute the module, it should raise an
+   :exc:`ImportError`, although any other exception raised during
+   :meth:`exec_module()` will be propagated.
 
-Module reprs
-------------
+In many cases, the finder and loader can be the same object; in such cases the
+:meth:`finder.find_spec()` would just return a spec with the loader set
+to ``self``.
 
-By default, all modules have a usable repr, however depending on the
-attributes set above, and hooks in the loader, you can more explicitly control
-the repr of module objects.
+Module loaders may opt in to creating the module object during loading
+by implementing a :meth:`create_module()` method.  It takes one argument,
+the module spec, and returns the new module object to use during loading.
+create_module() does not need to set any attributes on the module object.
+If the loader does not define create_module(), the import machinery will
+create the new module itself.
 
-Loaders may implement a :meth:`module_repr()` method which takes a single
-argument, the module object.  When ``repr(module)`` is called for a module
-with a loader supporting this protocol, whatever is returned from
-``module.__loader__.module_repr(module)`` is returned as the module's repr
-without further processing.  This return value must be a string.
+.. versionadded:: 3.4
+   The create_module() method of loaders.
 
-If the module has no ``__loader__`` attribute, or the loader has no
-:meth:`module_repr()` method, then the module object implementation itself
-will craft a default repr using whatever information is available.  It will
-try to use the ``module.__name__``, ``module.__file__``, and
-``module.__loader__`` as input into the repr, with defaults for whatever
-information is missing.
+.. versionchanged:: 3.4
+   The load_module() method was replaced by exec_module() and the import
+   machinery assumed all the boilerplate responsibilities of loading.
 
-Here are the exact rules used:
+   For compatibility with existing loaders, the import machinery will use
+   the :meth:`~importlib.abc.Loader.load_module()` method of loaders if it
+   exists and the loader does not also implement exec_module().  However,
+   load_module() has been deprecated and loaders should implement
+   exec_module() instead.
 
- * If the module has a ``__loader__`` and that loader has a
-   :meth:`module_repr()` method, call it with a single argument, which is the
-   module object.  The value returned is used as the module's repr.
+   The load_module() method must implement all the boilerplate loading
+   functionality described above in addition to executing the module.  All
+   the same constraints apply, with some additional clarification:
 
- * If an exception occurs in :meth:`module_repr()`, the exception is caught
-   and discarded, and the calculation of the module's repr continues as if
-   :meth:`module_repr()` did not exist.
+    * If there is an existing module object with the given name in
+      :data:`sys.modules`, the loader must use that existing module.
+      (Otherwise, :func:`imp.reload` will not work correctly.)  If the
+      named module does not exist in :data:`sys.modules`, the loader
+      must create a new module object and add it to :data:`sys.modules`.
 
- * If the module has a ``__file__`` attribute, this is used as part of the
-   module's repr.
+    * The module *must* exist in :data:`sys.modules` before the loader
+      executes the module code, to prevent unbounded recursion or multiple
+      loading.
 
- * If the module has no ``__file__`` but does have a ``__loader__`` that is not
-   ``None``, then the loader's repr is used as part of the module's repr.
+    * If loading fails, the loader must remove any modules it has inserted
+      into :data:`sys.modules`, but it must remove **only** the failing
+      module, and only if the loader itself has loaded it explicitly.
 
- * Otherwise, just use the module's ``__name__`` in the repr.
+Module spec
+-----------
 
-This example, from :pep:`420` shows how a loader can craft its own module
-repr::
+The import machinery uses a variety of information about each module
+during import, especially before loading.  Most of the information is
+common to all modules.  The purpose of a module's spec is to encapsulate
+this import-related information on a per-module basis.
 
-    class NamespaceLoader:
-        @classmethod
-        def module_repr(cls, module):
-            return "<module '{}' (namespace)>".format(module.__name__)
+Using a spec during import allows state to be transferred between import
+system components, e.g. between the finder that creates the module spec
+and the loader that executes it.  Most importantly, it allows the
+import machinery to perform the boilerplate operations of loading,
+whereas without a module spec the loader had that responsibility.
 
+See :class:`~importlib.machinery.ModuleSpec` for more specifics on what
+information a module's spec may hold.
+
+.. versionadded:: 3.4
+
+Import-related module attributes
+--------------------------------
+
+The import machinery fills in these attributes on each module object
+during loading, based on the module's spec, before the loader executes
+the module.
+
+.. attribute:: __name__
+
+   The ``__name__`` attribute must be set to the fully-qualified name of
+   the module.  This name is used to uniquely identify the module in
+   the import system.
+
+.. attribute:: __loader__
+
+   The ``__loader__`` attribute must be set to the loader object that
+   the import machinery used when loading the module.  This is mostly
+   for introspection, but can be used for additional loader-specific
+   functionality, for example getting data associated with a loader.
+
+.. attribute:: __package__
+
+   The module's ``__package__`` attribute must be set.  Its value must
+   be a string, but it can be the same value as its ``__name__``.  When
+   the module is a package, its ``__package__`` value should be set to
+   its ``__name__``.  When the module is not a package, ``__package__``
+   should be set to the empty string for top-level modules, or for
+   submodules, to the parent package's name.  See :pep:`366` for further
+   details.
+
+   This attribute is used instead of ``__name__`` to calculate explicit
+   relative imports for main modules, as defined in :pep:`366`.
+
+.. attribute:: __spec__
+
+   The ``__spec__`` attribute must be set to the module spec that was
+   used when importing the module.  This is used primarily for
+   introspection and during reloading.
+
+.. attribute:: __path__
+
+   If the module is a package (either regular or namespace), the module
+   object's ``__path__`` attribute must be set.  The value must be
+   iterable, but may be empty if ``__path__`` has no further significance.
+   If ``__path__`` is not empty, it must produce strings when iterated
+   over. More details on the semantics of ``__path__`` are given
+   :ref:`below <package-path-rules>`.
+
+   Non-package modules should not have a ``__path__`` attribute.
+
+.. attribute:: __file__
+.. attribute:: __cached__
+
+   ``__file__`` is optional. If set, this attribute's value must be a
+   string.  The import system may opt to leave ``__file__`` unset if it
+   has no semantic meaning (e.g. a module loaded from a database).
+
+   If ``__file__`` is set, it may also be appropriate to set the
+   ``__cached__`` attribute which is the path to any compiled version of
+   the code (e.g. byte-compiled file). The file does not need to exist
+   to set this attribute; the path can simply point to where the
+   compiled file would exist (see :pep:`3147`).
+
+   It is also appropriate to set ``__cached__`` when ``__file__`` is not
+   set.  However, that scenario is quite atypical.  Ultimately, the
+   loader is what makes use of ``__file__`` and/or ``__cached__``.  So
+   if a loader can load from a cached module but otherwise does not load
+   from a file, that atypical scenario may be appropriate.
 
 .. _package-path-rules:
 
@@ -464,9 +566,46 @@
 attribute, and this was typically the way namespace packages were implemented
 prior to :pep:`420`.  With the adoption of :pep:`420`, namespace packages no
 longer need to supply ``__init__.py`` files containing only ``__path__``
-manipulation code; the namespace loader automatically sets ``__path__``
+manipulation code; the import machinery automatically sets ``__path__``
 correctly for the namespace package.
 
+Module reprs
+------------
+
+By default, all modules have a usable repr, however depending on the
+attributes set above, and in the module's spec, you can more explicitly
+control the repr of module objects.
+
+If the module has a spec (``__spec__``), the import machinery will try
+to generate a repr from it.  If that fails or there is no spec, the import
+system will craft a default repr using whatever information is available
+on the module.  It will try to use the ``module.__name__``,
+``module.__file__``, and ``module.__loader__`` as input into the repr,
+with defaults for whatever information is missing.
+
+Here are the exact rules used:
+
+ * If the module has a ``__spec__`` attribute, the information in the spec
+   is used to generate the repr.  The "name", "loader", "origin", and
+   "has_location" attributes are consulted.
+
+ * If the module has a ``__file__`` attribute, this is used as part of the
+   module's repr.
+
+ * If the module has no ``__file__`` but does have a ``__loader__`` that is not
+   ``None``, then the loader's repr is used as part of the module's repr.
+
+ * Otherwise, just use the module's ``__name__`` in the repr.
+
+.. versionchanged:: 3.4
+   Use of loader.module_repr() has been deprecated and the module spec
+   is now used by the import machinery to generate a module repr.
+
+   For backward compatibility with Python 3.3, the module repr will be
+   generated by calling the loader's :meth:`module_repr()` method, if
+   defined, before trying either approach described above.  However, the
+   method is deprecated.
+
 
 The Path Based Finder
 =====================
@@ -531,7 +670,7 @@
 not be limited to this.
 
 As a meta path finder, the :term:`path based finder` implements the
-:meth:`find_module()` protocol previously described, however it exposes
+:meth:`find_spec()` protocol previously described, however it exposes
 additional hooks that can be used to customize how modules are found and
 loaded from the :term:`import path`.
 
@@ -553,8 +692,8 @@
 
 The :term:`path based finder` is a :term:`meta path finder`, so the import
 machinery begins the :term:`import path` search by calling the path
-based finder's :meth:`find_module()` method as described previously.  When
-the ``path`` argument to :meth:`find_module()` is given, it will be a
+based finder's :meth:`find_spec()` method as described previously.  When
+the ``path`` argument to :meth:`find_spec()` is given, it will be a
 list of string paths to traverse - typically a package's ``__path__``
 attribute for an import within that package.  If the ``path`` argument
 is ``None``, this indicates a top level import and :data:`sys.path` is used.
@@ -585,51 +724,70 @@
 argument, it should raise :exc:`ImportError`.
 
 If :data:`sys.path_hooks` iteration ends with no :term:`path entry finder`
-being returned, then the path based finder's :meth:`find_module()` method
+being returned, then the path based finder's :meth:`find_spec()` method
 will store ``None`` in :data:`sys.path_importer_cache` (to indicate that
 there is no finder for this path entry) and return ``None``, indicating that
 this :term:`meta path finder` could not find the module.
 
 If a :term:`path entry finder` *is* returned by one of the :term:`path entry
 hook` callables on :data:`sys.path_hooks`, then the following protocol is used
-to ask the finder for a module loader, which is then used to load the module.
-
+to ask the finder for a module spec, which is then used when loading the
+module.
 
 Path entry finder protocol
 --------------------------
 
 In order to support imports of modules and initialized packages and also to
 contribute portions to namespace packages, path entry finders must implement
-the :meth:`find_loader()` method.
+the :meth:`find_spec()` method.
 
-:meth:`find_loader()` takes one argument, the fully qualified name of the
-module being imported.  :meth:`find_loader()` returns a 2-tuple where the
-first item is the loader and the second item is a namespace :term:`portion`.
-When the first item (i.e. the loader) is ``None``, this means that while the
-path entry finder does not have a loader for the named module, it knows that the
-path entry contributes to a namespace portion for the named module.  This will
-almost always be the case where Python is asked to import a namespace package
-that has no physical presence on the file system.  When a path entry finder
-returns ``None`` for the loader, the second item of the 2-tuple return value
-must be a sequence, although it can be empty.
+:meth:`find_spec()` takes one argument, the fully qualified name of the
+module being imported.  :meth:`find_spec()` returns a fully populated
+spec for the module.  This spec will always have "loader" set (with one
+exception).
 
-If :meth:`find_loader()` returns a non-``None`` loader value, the portion is
-ignored and the loader is returned from the path based finder, terminating
-the search through the path entries.
+To indicate to the import machinery that the spec represents a namespace
+:term:`portion`. the path entry finder sets "loader" on the spec to
+``None`` and "submodule_search_locations" to a list containing the
+portion.
 
-For backwards compatibility with other implementations of the import
-protocol, many path entry finders also support the same,
-traditional :meth:`find_module()` method that meta path finders support.
-However path entry finder :meth:`find_module()` methods are never called
-with a ``path`` argument (they are expected to record the appropriate
-path information from the initial call to the path hook).
+.. versionchanged:: 3.4
+   find_spec() replaced find_loader() and find_module(), but of which
+   are now deprecated, but will be used if find_spec() is not defined.
 
-The :meth:`find_module()` method on path entry finders is deprecated,
-as it does not allow the path entry finder to contribute portions to
-namespace packages. Instead path entry finders should implement the
-:meth:`find_loader()` method as described above. If it exists on the path
-entry finder, the import system will always call :meth:`find_loader()`
-in preference to :meth:`find_module()`.
+   Older path entry finders may implement one of these two deprecated methods
+   instead of :meth:`find_spec()`.  The methods are still respected for the
+   sake of backward compatibility.  Howevever, if find_spec() is implemented
+   on the path entry finder, the legacy methods are ignored.
+
+   :meth:`find_loader()` takes one argument, the fully qualified name of the
+   module being imported.  :meth:`find_loader()` returns a 2-tuple where the
+   first item is the loader and the second item is a namespace :term:`portion`.
+   When the first item (i.e. the loader) is ``None``, this means that while the
+   path entry finder does not have a loader for the named module, it knows that
+   the path entry contributes to a namespace portion for the named module.
+   This will almost always be the case where Python is asked to import a
+   namespace package that has no physical presence on the file system.
+   When a path entry finder returns ``None`` for the loader, the second
+   item of the 2-tuple return value must be a sequence, although it can be
+   empty.
+
+   If :meth:`find_loader()` returns a non-``None`` loader value, the portion is
+   ignored and the loader is returned from the path based finder, terminating
+   the search through the path entries.
+
+   For backwards compatibility with other implementations of the import
+   protocol, many path entry finders also support the same,
+   traditional :meth:`find_module()` method that meta path finders support.
+   However path entry finder :meth:`find_module()` methods are never called
+   with a ``path`` argument (they are expected to record the appropriate
+   path information from the initial call to the path hook).
+
+   The :meth:`find_module()` method on path entry finders is deprecated,
+   as it does not allow the path entry finder to contribute portions to
+   namespace packages.  If both :meth:`find_loader()` and :meth:`find_module()`
+   exist on a path entry finder, the import system will always call
+   :meth:`find_loader()` in preference to :meth:`find_module()`.
 
 
 Replacing the standard import system
@@ -648,7 +806,7 @@
 To selectively prevent import of some modules from a hook early on the
 meta path (rather than disabling the standard import system entirely),
 it is sufficient to raise :exc:`ImportError` directly from
-:meth:`find_module` instead of returning ``None``. The latter indicates
+:meth:`find_spec` instead of returning ``None``. The latter indicates
 that the meta path search should continue. while raising an exception
 terminates it immediately.
 
@@ -690,6 +848,11 @@
 
 :pep:`338` defines executing modules as scripts.
 
+:pep:`451` adds the encapsulation of per-module import state in spec
+objects.  It also off-loads most of the boilerplate responsibilities of
+loaders back onto the import machinery.  These changes allow the
+deprecation of several APIs in the import system and also addition of new
+methods to finders and loaders.
 
 .. rubric:: Footnotes
 
diff --git a/Doc/whatsnew/3.4.rst b/Doc/whatsnew/3.4.rst
--- a/Doc/whatsnew/3.4.rst
+++ b/Doc/whatsnew/3.4.rst
@@ -236,6 +236,26 @@
 (Contributed by Nick Coghlan in :issue:`17827`, :issue:`17828` and
 :issue:`19619`)
 
+.. _pep-451:
+
+PEP 451: A ModuleSpec Type for the Import System
+================================================
+
+:pep:`451` provides an encapsulation of the information about a module
+that the import machinery will use to load it, (i.e. a module spec).
+This helps simplify both the import implementation and several
+import-related APIs.  The change is also a stepping stone for several
+future import-related improvements.
+
+https://mail.python.org/pipermail/python-dev/2013-November/130111.html
+
+The public-facing changes from the PEP are entirely backward-compatible.
+Furthermore, they should be transparent to everyone but importer
+authors.  Key finder and loader methods have been deprecated, but they
+will continue working.  New importers should use the new methods
+described in the PEP.  Existing importers should be updated to implement
+the new methods.
+
 
 Other Language Changes
 ======================
diff --git a/Lib/imp.py b/Lib/imp.py
--- a/Lib/imp.py
+++ b/Lib/imp.py
@@ -16,7 +16,7 @@
     # Platform doesn't support dynamic loading.
     load_dynamic = None
 
-from importlib._bootstrap import SourcelessFileLoader, _ERR_MSG
+from importlib._bootstrap import SourcelessFileLoader, _ERR_MSG, _SpecMethods
 
 from importlib import machinery
 from importlib import util
@@ -162,11 +162,17 @@
 
 
 def load_source(name, pathname, file=None):
-    _LoadSourceCompatibility(name, pathname, file).load_module(name)
-    module = sys.modules[name]
+    loader = _LoadSourceCompatibility(name, pathname, file)
+    spec = util.spec_from_file_location(name, pathname, loader=loader)
+    methods = _SpecMethods(spec)
+    if name in sys.modules:
+        module = methods.exec(sys.modules[name])
+    else:
+        module = methods.load()
     # To allow reloading to potentially work, use a non-hacked loader which
     # won't rely on a now-closed file object.
     module.__loader__ = machinery.SourceFileLoader(name, pathname)
+    module.__spec__.loader = module.__loader__
     return module
 
 
@@ -177,11 +183,17 @@
 
 def load_compiled(name, pathname, file=None):
     """**DEPRECATED**"""
-    _LoadCompiledCompatibility(name, pathname, file).load_module(name)
-    module = sys.modules[name]
+    loader = _LoadCompiledCompatibility(name, pathname, file)
+    spec = util.spec_from_file_location(name, pathname, loader=loader)
+    methods = _SpecMethods(spec)
+    if name in sys.modules:
+        module = methods.exec(sys.modules[name])
+    else:
+        module = methods.load()
     # To allow reloading to potentially work, use a non-hacked loader which
     # won't rely on a now-closed file object.
     module.__loader__ = SourcelessFileLoader(name, pathname)
+    module.__spec__.loader = module.__loader__
     return module
 
 
@@ -196,7 +208,13 @@
                 break
         else:
             raise ValueError('{!r} is not a package'.format(path))
-    return machinery.SourceFileLoader(name, path).load_module(name)
+    spec = util.spec_from_file_location(name, path,
+                                        submodule_search_locations=[])
+    methods = _SpecMethods(spec)
+    if name in sys.modules:
+        return methods.exec(sys.modules[name])
+    else:
+        return methods.load()
 
 
 def load_module(name, file, filename, details):
diff --git a/Lib/importlib/__init__.py b/Lib/importlib/__init__.py
--- a/Lib/importlib/__init__.py
+++ b/Lib/importlib/__init__.py
@@ -46,19 +46,42 @@
             finder.invalidate_caches()
 
 
-def find_loader(name, path=None):
-    """Find the loader for the specified module.
+def find_spec(name, path=None):
+    """Return the spec for the specified module.
 
     First, sys.modules is checked to see if the module was already imported. If
-    so, then sys.modules[name].__loader__ is returned. If that happens to be
+    so, then sys.modules[name].__spec__ is returned. If that happens to be
     set to None, then ValueError is raised. If the module is not in
-    sys.modules, then sys.meta_path is searched for a suitable loader with the
-    value of 'path' given to the finders. None is returned if no loader could
+    sys.modules, then sys.meta_path is searched for a suitable spec with the
+    value of 'path' given to the finders. None is returned if no spec could
     be found.
 
     Dotted names do not have their parent packages implicitly imported. You will
     most likely need to explicitly import all parent packages in the proper
-    order for a submodule to get the correct loader.
+    order for a submodule to get the correct spec.
+
+    """
+    if name not in sys.modules:
+        return _bootstrap._find_spec(name, path)
+    else:
+        module = sys.modules[name]
+        if module is None:
+            return None
+        try:
+            spec = module.__spec__
+        except AttributeError:
+            raise ValueError('{}.__spec__ is not set'.format(name))
+        else:
+            if spec is None:
+                raise ValueError('{}.__spec__ is None'.format(name))
+            return spec
+
+
+# XXX Deprecate...
+def find_loader(name, path=None):
+    """Return the loader for the specified module.
+
+    This is a backward-compatible wrapper around find_spec().
 
     """
     try:
@@ -71,7 +94,18 @@
         pass
     except AttributeError:
         raise ValueError('{}.__loader__ is not set'.format(name))
-    return _bootstrap._find_module(name, path)
+
+    spec = _bootstrap._find_spec(name, path)
+    # We won't worry about malformed specs (missing attributes).
+    if spec is None:
+        return None
+    if spec.loader is None:
+        if spec.submodule_search_locations is None:
+            raise ImportError('spec for {} missing loader'.format(name),
+                              name=name)
+        raise ImportError('namespace packages do not have loaders',
+                          name=name)
+    return spec.loader
 
 
 def import_module(name, package=None):
@@ -106,7 +140,11 @@
     """
     if not module or not isinstance(module, types.ModuleType):
         raise TypeError("reload() argument must be module")
-    name = module.__name__
+    try:
+        name = module.__spec__.name
+    except AttributeError:
+        name = module.__name__
+
     if sys.modules.get(name) is not module:
         msg = "module {} not in sys.modules"
         raise ImportError(msg.format(name), name=name)
@@ -118,13 +156,11 @@
         if parent_name and parent_name not in sys.modules:
             msg = "parent {!r} not in sys.modules"
             raise ImportError(msg.format(parent_name), name=parent_name)
-        loader = _bootstrap._find_module(name, None)
-        if loader is None:
-            raise ImportError(_bootstrap._ERR_MSG.format(name), name=name)
-        module.__loader__ = loader
-        loader.load_module(name)
+        spec = module.__spec__ = _bootstrap._find_spec(name, None, module)
+        methods = _bootstrap._SpecMethods(spec)
+        methods.exec(module)
         # The module may have replaced itself in sys.modules!
-        return sys.modules[module.__name__]
+        return sys.modules[name]
     finally:
         try:
             del _RELOADING[name]
diff --git a/Lib/importlib/_bootstrap.py b/Lib/importlib/_bootstrap.py
--- a/Lib/importlib/_bootstrap.py
+++ b/Lib/importlib/_bootstrap.py
@@ -84,13 +84,11 @@
     return (stat_info.st_mode & 0o170000) == mode
 
 
-# XXX Could also expose Modules/getpath.c:isfile()
 def _path_isfile(path):
     """Replacement for os.path.isfile."""
     return _path_is_mode_type(path, 0o100000)
 
 
-# XXX Could also expose Modules/getpath.c:isdir()
 def _path_isdir(path):
     """Replacement for os.path.isdir."""
     if not path:
@@ -128,6 +126,10 @@
     new.__dict__.update(old.__dict__)
 
 
+def _new_module(name):
+    return type(sys)(name)
+
+
 _code_type = type(_wrap.__code__)
 
 
@@ -232,6 +234,23 @@
         return '_DummyModuleLock({!r}) at {}'.format(self.name, id(self))
 
 
+class _ModuleLockManager:
+
+    def __init__(self, name):
+        self._name = name
+        self._lock = None
+
+    def __enter__(self):
+        try:
+            self._lock = _get_module_lock(self._name)
+        finally:
+            _imp.release_lock()
+        self._lock.acquire()
+
+    def __exit__(self, *args, **kwargs):
+        self._lock.release()
+
+
 # The following two functions are for consumption by Python/import.c.
 
 def _get_module_lock(name):
@@ -485,124 +504,6 @@
         print(message.format(*args), file=sys.stderr)
 
 
-class _ManageReload:
-
-    def __init__(self, name):
-        self._name = name
-
-    def __enter__(self):
-        self._is_reload = self._name in sys.modules
-
-    def __exit__(self, *args):
-        if any(arg is not None for arg in args) and not self._is_reload:
-            try:
-                del sys.modules[self._name]
-            except KeyError:
-                pass
-
-
-# Written as a class only because contextlib is not available.
-class _ModuleManager(_ManageReload):
-
-    """Context manager which returns the module to be loaded.
-
-    Does the proper unloading from sys.modules upon failure.
-
-    """
-
-    def __init__(self, name, *, reset_name=True):
-        """Prepare the context manager.
-
-        The reset_name argument specifies whether to unconditionally reset
-        the __name__ attribute if the module is found to be a reload.
-        """
-        super().__init__(name)
-        self._reset_name = reset_name
-
-    def __enter__(self):
-        super().__enter__()
-        self._module = sys.modules.get(self._name)
-        if not self._is_reload:
-            # This must be done before open() is called as the 'io' module
-            # implicitly imports 'locale' and would otherwise trigger an
-            # infinite loop.
-            self._module = type(_io)(self._name)
-            # This must be done before putting the module in sys.modules
-            # (otherwise an optimization shortcut in import.c becomes wrong)
-            self._module.__initializing__ = True
-            sys.modules[self._name] = self._module
-        elif self._reset_name:
-            try:
-                self._module.__name__ = self._name
-            except AttributeError:
-                pass
-        return self._module
-
-    def __exit__(self, *args):
-        self._module.__initializing__ = False
-        del self._module
-        super().__exit__(*args)
-
-
-def module_to_load(name, *, reset_name=True):
-    """Return a context manager which provides the module object to load.
-
-    If reset_name is true, reset the module's __name__ to 'name'.
-    """
-    # Hiding _ModuleManager behind a function for better naming.
-    return _ModuleManager(name, reset_name=reset_name)
-
-
-def _init_package_attrs(loader, module):
-    """Set __package__ and __path__ based on what loader.is_package() says."""
-    name = module.__name__
-    try:
-        is_package = loader.is_package(name)
-    except ImportError:
-        pass
-    else:
-        if is_package:
-            module.__package__ = name
-            module.__path__ = []
-        else:
-            module.__package__ = name.rpartition('.')[0]
-
-
-def _init_file_attrs(loader, module):
-    """Set __file__ and __path__ based on loader.get_filename()."""
-    try:
-        module.__file__ = loader.get_filename(module.__name__)
-    except ImportError:
-        pass
-    else:
-        if module.__name__ == module.__package__:
-            module.__path__.append(_path_split(module.__file__)[0])
-
-
-def set_package(fxn):
-    """Set __package__ on the returned module."""
-    def set_package_wrapper(*args, **kwargs):
-        module = fxn(*args, **kwargs)
-        if getattr(module, '__package__', None) is None:
-            module.__package__ = module.__name__
-            if not hasattr(module, '__path__'):
-                module.__package__ = module.__package__.rpartition('.')[0]
-        return module
-    _wrap(set_package_wrapper, fxn)
-    return set_package_wrapper
-
-
-def set_loader(fxn):
-    """Set __loader__ on the returned module."""
-    def set_loader_wrapper(self, *args, **kwargs):
-        module = fxn(self, *args, **kwargs)
-        if getattr(module, '__loader__', None) is None:
-            module.__loader__ = self
-        return module
-    _wrap(set_loader_wrapper, fxn)
-    return set_loader_wrapper
-
-
 def _check_name(method):
     """Decorator to verify that the module being requested matches the one the
     loader can handle.
@@ -656,6 +557,19 @@
     return loader
 
 
+def _load_module_shim(self, fullname):
+    """Load the specified module into sys.modules and return it."""
+    # XXX Deprecation Warning here...
+    spec = spec_from_loader(fullname, self)
+    methods = _SpecMethods(spec)
+    if fullname in sys.modules:
+        module = sys.modules[fullname]
+        methods.exec(module)
+        return sys.modules[fullname]
+    else:
+        return methods.load()
+
+
 def _validate_bytecode_header(data, source_stats=None, name=None, path=None):
     """Validate the header of the passed-in bytecode against source_stats (if
     given) and returning the bytecode that can be compiled by compile().
@@ -745,6 +659,556 @@
     return newline_decoder.decode(source_bytes.decode(encoding[0]))
 
 
+# Module specifications #######################################################
+
+def _module_repr(module):
+    # The implementation of ModuleType__repr__().
+    loader = getattr(module, '__loader__', None)
+    if hasattr(loader, 'module_repr'):
+        # XXX Deprecation Warning here...
+        try:
+            return loader.module_repr(module)
+        except Exception:
+            pass
+    try:
+        spec = module.__spec__
+    except AttributeError:
+        pass
+    else:
+        if spec is not None:
+            return _SpecMethods(spec).module_repr()
+
+    # We could use module.__class__.__name__ instead of 'module' in the
+    # various repr permutations.
+    try:
+        name = module.__name__
+    except AttributeError:
+        name = '?'
+    try:
+        filename = module.__file__
+    except AttributeError:
+        if loader is None:
+            return '<module {!r}>'.format(name)
+        else:
+            return '<module {!r} ({!r})>'.format(name, loader)
+    else:
+        return '<module {!r} from {!r}>'.format(name, filename)
+
+
+class _installed_safely:
+
+    def __init__(self, module):
+        self._module = module
+        self._spec = module.__spec__
+
+    def __enter__(self):
+        # This must be done before putting the module in sys.modules
+        # (otherwise an optimization shortcut in import.c becomes
+        # wrong)
+        self._spec._initializing = True
+        sys.modules[self._spec.name] = self._module
+
+    def __exit__(self, *args):
+        try:
+            spec = self._spec
+            if any(arg is not None for arg in args):
+                try:
+                    del sys.modules[spec.name]
+                except KeyError:
+                    pass
+            else:
+                _verbose_message('import {!r} # {!r}', spec.name, spec.loader)
+        finally:
+            self._spec._initializing = False
+
+
+class ModuleSpec:
+    """The specification for a module, used for loading.
+
+    A module's spec is the source for information about the module.  For
+    data associated with the module, including source, use the spec's
+    loader.
+
+    `name` is the absolute name of the module.  `loader` is the loader
+    to use when loading the module.  `parent` is the name of the
+    package the module is in.  The parent is derived from the name.
+
+    `is_package` determines if the module is considered a package or
+    not.  On modules this is reflected by the `__path__` attribute.
+
+    `origin` is the specific location used by the loader from which to
+    load the module, if that information is available.  When filename is
+    set, origin will match.
+
+    `has_location` indicates that a spec's "origin" reflects a location.
+    When this is True, `__file__` attribute of the module is set.
+
+    `cached` is the location of the cached bytecode file, if any.  It
+    corresponds to the `__cached__` attribute.
+
+    `submodule_search_locations` is the sequence of path entries to
+    search when importing submodules.  If set, is_package should be
+    True--and False otherwise.
+
+    Packages are simply modules that (may) have submodules.  If a spec
+    has a non-None value in `submodule_search_locations`, the import
+    system will consider modules loaded from the spec as packages.
+
+    Only finders (see importlib.abc.MetaPathFinder and
+    importlib.abc.PathEntryFinder) should modify ModuleSpec instances.
+
+    """
+
+    def __init__(self, name, loader, *, origin=None, loader_state=None,
+                 is_package=None):
+        self.name = name
+        self.loader = loader
+        self.origin = origin
+        self.loader_state = loader_state
+        self.submodule_search_locations = [] if is_package else None
+
+        # file-location attributes
+        self._set_fileattr = False
+        self._cached = None
+
+    def __repr__(self):
+        args = ['name={!r}'.format(self.name),
+                'loader={!r}'.format(self.loader)]
+        if self.origin is not None:
+            args.append('origin={!r}'.format(self.origin))
+        if self.submodule_search_locations is not None:
+            args.append('submodule_search_locations={}'
+                        .format(self.submodule_search_locations))
+        return '{}({})'.format(self.__class__.__name__, ', '.join(args))
+
+    def __eq__(self, other):
+        smsl = self.submodule_search_locations
+        try:
+            return (self.name == other.name and
+                    self.loader == other.loader and
+                    self.origin == other.origin and
+                    smsl == other.submodule_search_locations and
+                    self.cached == other.cached and
+                    self.has_location == other.has_location)
+        except AttributeError:
+            return False
+
+    @property
+    def cached(self):
+        if self._cached is None:
+            if self.origin is not None and self._set_fileattr:
+                filename = self.origin
+                if filename.endswith(tuple(SOURCE_SUFFIXES)):
+                    try:
+                        self._cached = cache_from_source(filename)
+                    except NotImplementedError:
+                        pass
+                elif filename.endswith(tuple(BYTECODE_SUFFIXES)):
+                    self._cached = filename
+        return self._cached
+
+    @cached.setter
+    def cached(self, cached):
+        self._cached = cached
+
+    @property
+    def parent(self):
+        """The name of the module's parent."""
+        if self.submodule_search_locations is None:
+            return self.name.rpartition('.')[0]
+        else:
+            return self.name
+
+    @property
+    def has_location(self):
+        return self._set_fileattr
+
+
+def spec_from_loader(name, loader, *, origin=None, is_package=None):
+    """Return a module spec based on various loader methods."""
+
+#    if hasattr(loader, 'get_data'):
+    if hasattr(loader, 'get_filename'):
+        if is_package is None:
+            return spec_from_file_location(name, loader=loader)
+        search = [] if is_package else None
+        return spec_from_file_location(name, loader=loader,
+                                       submodule_search_locations=search)
+
+    if is_package is None:
+        if hasattr(loader, 'is_package'):
+            try:
+                is_package = loader.is_package(name)
+            except ImportError:
+                is_package = None  # aka, undefined
+        else:
+            # the default
+            is_package = False
+
+    return ModuleSpec(name, loader, origin=origin, is_package=is_package)
+
+
+_POPULATE = object()
+
+
+def spec_from_file_location(name, location=None, *, loader=None,
+                            submodule_search_locations=_POPULATE):
+    """Return a module spec based on a file location.
+
+    To indicate that the module is a package, set
+    submodule_search_locations to a list of directory paths.  An
+    empty list is sufficient, though its not otherwise useful to the
+    import system.
+
+    The loader must take a spec as its only __init__() arg.
+
+    """
+    if location is None:
+        # The caller may simply want a partially populated location-
+        # oriented spec.  So we set the location to a bogus value and
+        # fill in as much as we can.
+        location = '<unknown>'
+        if hasattr(loader, 'get_filename'):
+            # ExecutionLoader
+            try:
+                location = loader.get_filename(name)
+            except ImportError:
+                pass
+
+    # If the location is on the filesystem, but doesn't actually exist,
+    # we could return None here, indicating that the location is not
+    # valid.  However, we don't have a good way of testing since an
+    # indirect location (e.g. a zip file or URL) will look like a
+    # non-existent file relative to the filesystem.
+
+    spec = ModuleSpec(name, loader, origin=location)
+    spec._set_fileattr = True
+
+    # Pick a loader if one wasn't provided.
+    if loader is None:
+        for loader_class, suffixes in _get_supported_file_loaders():
+            if location.endswith(tuple(suffixes)):
+                loader = loader_class(name, location)
+                spec.loader = loader
+                break
+        else:
+            return None
+
+    # Set submodule_search_paths appropriately.
+    if submodule_search_locations is _POPULATE:
+        # Check the loader.
+        if hasattr(loader, 'is_package'):
+            try:
+                is_package = loader.is_package(name)
+            except ImportError:
+                pass
+            else:
+                if is_package:
+                    spec.submodule_search_locations = []
+    else:
+        spec.submodule_search_locations = submodule_search_locations
+    if spec.submodule_search_locations == []:
+        if location:
+            dirname = _path_split(location)[0]
+            spec.submodule_search_locations.append(dirname)
+
+    return spec
+
+
+def _spec_from_module(module, loader=None, origin=None):
+    # This function is meant for use in _setup().
+    try:
+        spec = module.__spec__
+    except AttributeError:
+        pass
+    else:
+        if spec is not None:
+            return spec
+
+    name = module.__name__
+    if loader is None:
+        try:
+            loader = module.__loader__
+        except AttributeError:
+            # loader will stay None.
+            pass
+    try:
+        location = module.__file__
+    except AttributeError:
+        location = None
+    if origin is None:
+        if location is None:
+            try:
+                origin = loader._ORIGIN
+            except AttributeError:
+                origin = None
+        else:
+            origin = location
+    try:
+        cached = module.__cached__
+    except AttributeError:
+        cached = None
+    try:
+        submodule_search_locations = list(module.__path__)
+    except AttributeError:
+        submodule_search_locations = None
+
+    spec = ModuleSpec(name, loader, origin=origin)
+    spec._set_fileattr = False if location is None else True
+    spec.cached = cached
+    spec.submodule_search_locations = submodule_search_locations
+    return spec
+
+
+class _SpecMethods:
+
+    """Convenience wrapper around spec objects to provide spec-specific
+    methods."""
+
+    def __init__(self, spec):
+        self.spec = spec
+
+    @classmethod
+    def from_module(cls, module):
+        """Create a spec from a module's attributes."""
+        try:
+            spec = module.__spec__
+        except AttributeError:
+            try:
+                loader = spec.__loader__
+            except AttributeError:
+                spec = _find_spec(module.__name__)
+                if spec is None:
+                    spec = spec_from_loader(module.__name__, loader)
+            else:
+                spec = spec_from_loader(module.__name__, loader)
+        return cls(spec)
+
+    def module_repr(self):
+        """Return the repr to use for the module."""
+        # We mostly replicate _module_repr() using the spec attributes.
+        spec = self.spec
+        name = '?' if spec.name is None else spec.name
+        if spec.origin is None:
+            if spec.loader is None:
+                return '<module {!r}>'.format(name)
+            else:
+                return '<module {!r} ({!r})>'.format(name, spec.loader)
+        else:
+            if spec.has_location:
+                return '<module {!r} from {!r}>'.format(name, spec.origin)
+            else:
+                return '<module {!r} ({})>'.format(spec.name, spec.origin)
+
+    def init_module_attrs(self, module, *, _override=False, _force_name=True):
+        """Set the module's attributes.
+
+        All missing import-related module attributes will be set.  Here
+        is how the spec attributes map onto the module:
+
+        spec.name -> module.__name__
+        spec.loader -> module.__loader__
+        spec.parent -> module.__package__
+        spec -> module.__spec__
+
+        Optional:
+        spec.origin -> module.__file__ (if spec.set_fileattr is true)
+        spec.cached -> module.__cached__ (if __file__ also set)
+        spec.submodule_search_locations -> module.__path__ (if set)
+
+        """
+        spec = self.spec
+
+        # The passed in module may be not support attribute assignment,
+        # in which case we simply don't set the attributes.
+
+        # __name__
+        if (_override or _force_name or
+            getattr(module, '__name__', None) is None):
+            try:
+                module.__name__ = spec.name
+            except AttributeError:
+                pass
+
+        # __loader__
+        if _override or getattr(module, '__loader__', None) is None:
+            loader = spec.loader
+            if loader is None:
+                # A backward compatibility hack.
+                if spec.submodule_search_locations is not None:
+                    loader = _NamespaceLoader.__new__(_NamespaceLoader)
+                    loader._path = spec.submodule_search_locations
+            try:
+                module.__loader__ = loader
+            except AttributeError:
+                pass
+
+        # __package__
+        if _override or getattr(module, '__package__', None) is None:
+            try:
+                module.__package__ = spec.parent
+            except AttributeError:
+                pass
+
+        # __spec__
+        try:
+            module.__spec__ = spec
+        except AttributeError:
+            pass
+
+        # __path__
+        if _override or getattr(module, '__path__', None) is None:
+            if spec.submodule_search_locations is not None:
+                try:
+                    module.__path__ = spec.submodule_search_locations
+                except AttributeError:
+                    pass
+
+        if spec.has_location:
+            # __file__
+            if _override or getattr(module, '__file__', None) is None:
+                try:
+                    module.__file__ = spec.origin
+                except AttributeError:
+                    pass
+
+            # __cached__
+            if _override or getattr(module, '__cached__', None) is None:
+                if spec.cached is not None:
+                    try:
+                        module.__cached__ = spec.cached
+                    except AttributeError:
+                        pass
+
+    def create(self):
+        """Return a new module to be loaded.
+
+        The import-related module attributes are also set with the
+        appropriate values from the spec.
+
+        """
+        spec = self.spec
+        # Typically loaders will not implement create_module().
+        if hasattr(spec.loader, 'create_module'):
+            # If create_module() returns `None` it means the default
+            # module creation should be used.
+            module = spec.loader.create_module(spec)
+        else:
+            module = None
+        if module is None:
+            # This must be done before open() is ever called as the 'io'
+            # module implicitly imports 'locale' and would otherwise
+            # trigger an infinite loop.
+            module = _new_module(spec.name)
+        self.init_module_attrs(module)
+        return module
+
+    def _exec(self, module):
+        """Do everything necessary to execute the module.
+
+        The namespace of `module` is used as the target of execution.
+        This method uses the loader's `exec_module()` method.
+
+        """
+        self.spec.loader.exec_module(module)
+
+    # Used by importlib.reload() and _load_module_shim().
+    def exec(self, module):
+        """Execute the spec in an existing module's namespace."""
+        name = self.spec.name
+        _imp.acquire_lock()
+        with _ModuleLockManager(name):
+            if sys.modules.get(name) is not module:
+                msg = 'module {} not in sys.modules'.format(name)
+                raise ImportError(msg, name=name)
+            if self.spec.loader is None:
+                if self.spec.submodule_search_locations is None:
+                    raise ImportError('missing loader', name=self.spec.name)
+                # namespace package
+                self.init_module_attrs(module, _override=True)
+                return module
+            self.init_module_attrs(module, _override=True)
+            if not hasattr(self.spec.loader, 'exec_module'):
+                # XXX DeprecationWarning goes here...
+                self.spec.loader.load_module(name)
+            else:
+                self._exec(module)
+        return sys.modules[name]
+
+    def _load_backward_compatible(self):
+        # XXX DeprecationWarning goes here...
+        spec = self.spec
+        # The module must be in sys.modules!
+        spec.loader.load_module(spec.name)
+        module = sys.modules[spec.name]
+        if getattr(module, '__loader__', None) is None:
+            try:
+                module.__loader__ = spec.loader
+            except AttributeError:
+                pass
+        if getattr(module, '__package__', None) is None:
+            try:
+                # Since module.__path__ may not line up with
+                # spec.submodule_search_paths, we can't necessarily rely
+                # on spec.parent here.
+                module.__package__ = module.__name__
+                if not hasattr(module, '__path__'):
+                    module.__package__ = spec.name.rpartition('.')[0]
+            except AttributeError:
+                pass
+        if getattr(module, '__spec__', None) is None:
+            try:
+                module.__spec__ = spec
+            except AttributeError:
+                pass
+        return module
+
+    # XXX If we don't end up using this for pythonrun.c/runpy, we should
+    #     get rid of it.
+    def _load_existing(self, module):
+        """Exec the spec'ed module into an existing module's namespace."""
+        # For use by runpy.
+        with _installed_safely(module):
+            loaded = self.exec(module)
+        return loaded
+
+    def _load_unlocked(self):
+        # A helper for direct use by the import system.
+        if self.spec.loader is not None:
+            # not a namespace package
+            if not hasattr(self.spec.loader, 'exec_module'):
+                return self._load_backward_compatible()
+
+        module = self.create()
+        with _installed_safely(module):
+            if self.spec.loader is None:
+                if self.spec.submodule_search_locations is None:
+                    raise ImportError('missing loader', name=self.spec.name)
+                # A namespace package so do nothing.
+            else:
+                self._exec(module)
+
+        # We don't ensure that the import-related module attributes get
+        # set in the sys.modules replacement case.  Such modules are on
+        # their own.
+        return sys.modules[self.spec.name]
+
+    # A method used during testing of _load_unlocked() and by
+    # _load_module_shim().
+    def load(self):
+        """Return a new module object, loaded by the spec's loader.
+
+        The module is not added to its parent.
+
+        If a module is already in sys.modules, that existing module gets
+        clobbered.
+
+        """
+        _imp.acquire_lock()
+        with _ModuleLockManager(self.spec.name):
+            return self._load_unlocked()
+
+
 # Loaders #####################################################################
 
 class BuiltinImporter:
@@ -756,9 +1220,19 @@
 
     """
 
+    @staticmethod
+    def module_repr(module):
+        # XXX deprecate
+        return '<module {!r} (built-in)>'.format(module.__name__)
+
     @classmethod
-    def module_repr(cls, module):
-        return '<module {!r} (built-in)>'.format(module.__name__)
+    def find_spec(cls, fullname, path=None, target=None):
+        if path is not None:
+            return None
+        if _imp.is_builtin(fullname):
+            return spec_from_loader(fullname, cls, origin='built-in')
+        else:
+            return None
 
     @classmethod
     def find_module(cls, fullname, path=None):
@@ -767,18 +1241,26 @@
         If 'path' is ever specified then the search is considered a failure.
 
         """
-        if path is not None:
-            return None
-        return cls if _imp.is_builtin(fullname) else None
+        spec = cls.find_spec(fullname, path)
+        return spec.loader if spec is not None else None
+
+    @staticmethod
+    def exec_module(module):
+        spec = module.__spec__
+        name = spec.name
+        if not _imp.is_builtin(name):
+            raise ImportError('{} is not a built-in module'.format(name),
+                              name=name)
+        _call_with_frames_removed(_imp.init_builtin, name)
+        # Have to manually initialize attributes since init_builtin() is not
+        # going to do it for us.
+        # XXX: Create _imp.exec_builtin(module)
+        _SpecMethods(spec).init_module_attrs(sys.modules[name])
 
     @classmethod
-    @set_package
-    @set_loader
-    @_requires_builtin
     def load_module(cls, fullname):
         """Load a built-in module."""
-        with _ManageReload(fullname):
-            return _call_with_frames_removed(_imp.init_builtin, fullname)
+        return _load_module_shim(cls, fullname)
 
     @classmethod
     @_requires_builtin
@@ -796,6 +1278,7 @@
     @_requires_builtin
     def is_package(cls, fullname):
         """Return False as built-in modules are never packages."""
+        # XXX DeprecationWarning here...
         return False
 
 
@@ -808,26 +1291,36 @@
 
     """
 
+    @staticmethod
+    def module_repr(m):
+        # XXX deprecate
+        return '<module {!r} (frozen)>'.format(m.__name__)
+
     @classmethod
-    def module_repr(cls, m):
-        return '<module {!r} (frozen)>'.format(m.__name__)
+    def find_spec(cls, fullname, path=None, target=None):
+        if _imp.is_frozen(fullname):
+            return spec_from_loader(fullname, cls, origin='frozen')
+        else:
+            return None
 
     @classmethod
     def find_module(cls, fullname, path=None):
         """Find a frozen module."""
         return cls if _imp.is_frozen(fullname) else None
 
+    @staticmethod
+    def exec_module(module):
+        name = module.__spec__.name
+        if not _imp.is_frozen(name):
+            raise ImportError('{} is not a frozen module'.format(name),
+                              name=name)
+        code = _call_with_frames_removed(_imp.get_frozen_object, name)
+        exec(code, module.__dict__)
+
     @classmethod
-    @set_package
-    @set_loader
-    @_requires_frozen
     def load_module(cls, fullname):
         """Load a frozen module."""
-        with _ManageReload(fullname):
-            m = _call_with_frames_removed(_imp.init_frozen, fullname)
-            # Let our own module_repr() method produce a suitable repr.
-            del m.__file__
-            return m
+        return _load_module_shim(cls, fullname)
 
     @classmethod
     @_requires_frozen
@@ -850,8 +1343,7 @@
 
 class WindowsRegistryFinder:
 
-    """Meta path finder for modules declared in the Windows registry.
-    """
+    """Meta path finder for modules declared in the Windows registry."""
 
     REGISTRY_KEY = (
         'Software\\Python\\PythonCore\\{sys_version}'
@@ -884,8 +1376,8 @@
         return filepath
 
     @classmethod
-    def find_module(cls, fullname, path=None):
-        """Find module named in the registry."""
+    def find_spec(cls, fullname, path=None, target=None):
+        # XXX untested! Need a Windows person to write tests (otherwise mock out appropriately)
         filepath = cls._search_registry(fullname)
         if filepath is None:
             return None
@@ -895,7 +1387,18 @@
             return None
         for loader, suffixes in _get_supported_file_loaders():
             if filepath.endswith(tuple(suffixes)):
-                return loader(fullname, filepath)
+                spec = spec_from_loader(fullname, loader(fullname, filepath),
+                                        origin=filepath)
+                return spec
+
+    @classmethod
+    def find_module(cls, fullname, path=None):
+        """Find module named in the registry."""
+        spec = self.find_spec(fullname, path)
+        if spec is not None:
+            return spec.loader
+        else:
+            return None
 
 
 class _LoaderBasics:
@@ -903,6 +1406,7 @@
     """Base class of common code needed by both SourceLoader and
     SourcelessFileLoader."""
 
+    # XXX deprecate?
     def is_package(self, fullname):
         """Concrete implementation of InspectLoader.is_package by checking if
         the path returned by get_filename has a filename of '__init__.py'."""
@@ -911,32 +1415,15 @@
         tail_name = fullname.rpartition('.')[2]
         return filename_base == '__init__' and tail_name != '__init__'
 
-    def init_module_attrs(self, module):
-        """Set various attributes on the module.
+    def exec_module(self, module):
+        """Execute the module."""
+        code = self.get_code(module.__name__)
+        if code is None:
+            raise ImportError('cannot load module {!r} when get_code() '
+                              'returns None'.format(module.__name__))
+        _call_with_frames_removed(exec, code, module.__dict__)
 
-        ExecutionLoader.init_module_attrs() is used to set __loader__,
-        __package__, __file__, and optionally __path__. The __cached__ attribute
-        is set using imp.cache_from_source() and __file__.
-        """
-        module.__loader__ = self  # Loader
-        _init_package_attrs(self, module)  # InspectLoader
-        _init_file_attrs(self, module)  # ExecutionLoader
-        if hasattr(module, '__file__'):  # SourceLoader
-            try:
-                module.__cached__ = cache_from_source(module.__file__)
-            except NotImplementedError:
-                pass
-
-    def load_module(self, fullname):
-        """Load the specified module into sys.modules and return it."""
-        with module_to_load(fullname) as module:
-            self.init_module_attrs(module)
-            code = self.get_code(fullname)
-            if code is None:
-                raise ImportError('cannot load module {!r} when get_code() '
-                                  'returns None'.format(fullname))
-            _call_with_frames_removed(exec, code, module.__dict__)
-            return module
+    load_module = _load_module_shim
 
 
 class SourceLoader(_LoaderBasics):
@@ -1063,7 +1550,9 @@
     @_check_name
     def load_module(self, fullname):
         """Load a module from a file."""
-        # Issue #14857: Avoid the zero-argument form so the implementation
+        # The only reason for this method is for the name check.
+
+        # Issue #14857: Avoid the zero-argument form of super so the implementation
         # of that form can be updated without breaking the frozen module
         return super(FileLoader, self).load_module(fullname)
 
@@ -1125,10 +1614,6 @@
 
     """Loader which handles sourceless file imports."""
 
-    def init_module_attrs(self, module):
-        super().init_module_attrs(module)
-        module.__cached__ = module.__file__
-
     def get_code(self, fullname):
         path = self.get_filename(fullname)
         data = self.get_data(path)
@@ -1156,18 +1641,22 @@
         self.name = name
         self.path = path
 
+    def exec_module(self, module):
+        # XXX create _imp.exec_dynamic()
+        spec = module.__spec__
+        fullname = spec.name
+        module = _call_with_frames_removed(_imp.load_dynamic,
+                                               fullname, self.path)
+        _verbose_message('extension module loaded from {!r}', self.path)
+        if self.is_package(fullname) and not hasattr(module, '__path__'):
+            module.__path__ = [_path_split(self.path)[0]]
+        _SpecMethods(spec).init_module_attrs(module)
+
+    # XXX deprecate
     @_check_name
-    @set_package
-    @set_loader
     def load_module(self, fullname):
         """Load an extension module."""
-        with _ManageReload(fullname):
-            module = _call_with_frames_removed(_imp.load_dynamic,
-                                               fullname, self.path)
-            _verbose_message('extension module loaded from {!r}', self.path)
-            if self.is_package(fullname) and not hasattr(module, '__path__'):
-                module.__path__ = [_path_split(self.path)[0]]
-            return module
+        return _load_module_shim(self, fullname)
 
     def is_package(self, fullname):
         """Return True if the extension module is a package."""
@@ -1220,11 +1709,12 @@
         # 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)
+            spec = 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
+            if spec is not None and spec.loader is None:
+                if spec.submodule_search_locations:
+                    self._path = spec.submodule_search_locations
             self._last_parent_path = parent_path     # Save the copy
         return self._path
 
@@ -1244,10 +1734,12 @@
         self._path.append(item)
 
 
-class NamespaceLoader:
+# We use this exclusively in init_module_attrs() for backward-compatibility.
+class _NamespaceLoader:
     def __init__(self, name, path, path_finder):
         self._path = _NamespacePath(name, path, path_finder)
 
+    # XXX Deprecate
     @classmethod
     def module_repr(cls, module):
         return '<module {!r} (namespace)>'.format(module.__name__)
@@ -1261,17 +1753,11 @@
     def get_code(self, fullname):
         return compile('', '<string>', 'exec', dont_inherit=True)
 
-    def init_module_attrs(self, module):
-        module.__loader__ = self
-        module.__package__ = module.__name__
-
+    # XXX Deprecate
     def load_module(self, fullname):
         """Load a namespace module."""
         _verbose_message('namespace module loaded with path {!r}', self._path)
-        with module_to_load(fullname) as module:
-            self.init_module_attrs(module)
-            module.__path__ = self._path
-            return module
+        return _load_module_shim(self, fullname)
 
 
 # Finders #####################################################################
@@ -1323,7 +1809,20 @@
         return finder
 
     @classmethod
-    def _get_loader(cls, fullname, path):
+    def _legacy_get_spec(cls, fullname, finder):
+        if hasattr(finder, 'find_loader'):
+            loader, portions = finder.find_loader(fullname)
+        else:
+            loader = finder.find_module(fullname)
+            portions = None
+        if loader is not None:
+            return spec_from_loader(fullname, loader)
+        spec = ModuleSpec(fullname, None)
+        spec.submodule_search_locations = portions
+        return spec
+
+    @classmethod
+    def _get_spec(cls, fullname, path, target=None):
         """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__
@@ -1333,38 +1832,58 @@
                 continue
             finder = cls._path_importer_cache(entry)
             if finder is not None:
-                if hasattr(finder, 'find_loader'):
-                    loader, portions = finder.find_loader(fullname)
+                if hasattr(finder, 'find_spec'):
+                    spec = finder.find_spec(fullname, target)
                 else:
-                    loader = finder.find_module(fullname)
-                    portions = []
-                if loader is not None:
-                    # We found a loader: return it immediately.
-                    return loader, namespace_path
+                    spec = cls._legacy_get_spec(fullname, finder)
+                if spec is None:
+                    continue
+                if spec.loader is not None:
+                    return spec
+                portions = spec.submodule_search_locations
+                if portions is None:
+                    raise ImportError('spec missing loader')
                 # 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
+            spec = ModuleSpec(fullname, None)
+            spec.submodule_search_locations = namespace_path
+            return spec
+
+    @classmethod
+    def find_spec(cls, fullname, path=None, target=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
+        spec = cls._get_spec(fullname, path, target)
+        if spec is None:
+            return None
+        elif spec.loader is None:
+            namespace_path = spec.submodule_search_locations
+            if namespace_path:
+                # We found at least one namespace path.  Return a
+                #  spec which can create the namespace package.
+                spec.origin = 'namespace'
+                spec.submodule_search_locations = _NamespacePath(fullname, namespace_path, cls._get_spec)
+                return spec
+            else:
+                return None
+        else:
+            return spec
 
     @classmethod
     def find_module(cls, fullname, path=None):
-        """Find the module on sys.path or 'path' based on sys.path_hooks and
+        """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
-        loader, namespace_path = cls._get_loader(fullname, path)
-        if loader is not None:
-            return loader
-        else:
-            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
+        # XXX Deprecation warning here.
+        spec = cls.find_spec(fullname, path)
+        if spec is None:
+            return None
+        return spec.loader
 
 
 class FileFinder:
@@ -1399,6 +1918,24 @@
     def find_loader(self, fullname):
         """Try to find a loader for the specified module, or the namespace
         package portions. Returns (loader, list-of-portions)."""
+        spec = self.find_spec(fullname)
+        if spec is None:
+            return None, []
+        return spec.loader, spec.submodule_search_locations or []
+
+    def _get_spec(self, loader_class, fullname, path, submodule_search_locations, target):
+        loader = loader_class(fullname, path)
+        try:
+            get_spec = loader._get_spec
+        except AttributeError:
+            return spec_from_file_location(fullname, path, loader=loader,
+                                           submodule_search_locations=submodule_search_locations)
+        else:
+            return get_spec(fullname, path, submodule_search_locations, target)
+
+    def find_spec(self, fullname, target=None):
+        """Try to find a loader for the specified module, or the namespace
+        package portions. Returns (loader, list-of-portions)."""
         is_namespace = False
         tail_module = fullname.rpartition('.')[2]
         try:
@@ -1418,26 +1955,28 @@
         # Check if the module is the name of a directory (and thus a package).
         if cache_module in cache:
             base_path = _path_join(self.path, tail_module)
-            for suffix, loader in self._loaders:
+            for suffix, loader_class in self._loaders:
                 init_filename = '__init__' + suffix
                 full_path = _path_join(base_path, init_filename)
                 if _path_isfile(full_path):
-                    return (loader(fullname, full_path), [base_path])
+                    return self._get_spec(loader_class, fullname, full_path, [base_path], target)
             else:
                 # If a namespace package, return the path if we don't
                 #  find a module in the next section.
                 is_namespace = _path_isdir(base_path)
         # Check for a file w/ a proper suffix exists.
-        for suffix, loader in self._loaders:
+        for suffix, loader_class in self._loaders:
             full_path = _path_join(self.path, tail_module + suffix)
             _verbose_message('trying {}'.format(full_path), verbosity=2)
             if cache_module + suffix in cache:
                 if _path_isfile(full_path):
-                    return (loader(fullname, full_path), [])
+                    return self._get_spec(loader_class, fullname, full_path, None, target)
         if is_namespace:
             _verbose_message('possible namespace for {}'.format(base_path))
-            return (None, [base_path])
-        return (None, [])
+            spec = ModuleSpec(fullname, None)
+            spec.submodule_search_locations = [base_path]
+            return spec
+        return None
 
     def _fill_cache(self):
         """Fill the cache of potential modules and packages for this directory."""
@@ -1516,23 +2055,43 @@
     return '{}.{}'.format(base, name) if name else base
 
 
-def _find_module(name, path):
+def _find_spec(name, path, target=None):
     """Find a module's loader."""
     if not sys.meta_path:
         _warnings.warn('sys.meta_path is empty', ImportWarning)
+    # We check sys.modules here for the reload case.  While a passed-in
+    # target will usually indicate a reload there is no guarantee, whereas
+    # sys.modules provides one.
     is_reload = name in sys.modules
     for finder in sys.meta_path:
         with _ImportLockContext():
-            loader = finder.find_module(name, path)
-        if loader is not None:
+            try:
+                find_spec = finder.find_spec
+            except AttributeError:
+                loader = finder.find_module(name, path)
+                if loader is None:
+                    continue
+                spec = spec_from_loader(name, loader)
+            else:
+                spec = find_spec(name, path, target)
+        if spec is not None:
             # The parent import may have already imported this module.
-            if is_reload or name not in sys.modules:
-                return loader
+            if not is_reload and name in sys.modules:
+                module = sys.modules[name]
+                try:
+                    __spec__ = module.__spec__
+                except AttributeError:
+                    # We use the found spec since that is the one that
+                    # we would have used if the parent module hadn't
+                    # beaten us to the punch.
+                    return spec
+                else:
+                    if __spec__ is None:
+                        return spec
+                    else:
+                        return __spec__
             else:
-                try:
-                    return sys.modules[name].__loader__
-                except AttributeError:
-                    return loader
+                return spec
     else:
         return None
 
@@ -1566,54 +2125,28 @@
         # Crazy side-effects!
         if name in sys.modules:
             return sys.modules[name]
-        # Backwards-compatibility; be nicer to skip the dict lookup.
         parent_module = sys.modules[parent]
         try:
             path = parent_module.__path__
         except AttributeError:
             msg = (_ERR_MSG + '; {} is not a package').format(name, parent)
             raise ImportError(msg, name=name)
-    loader = _find_module(name, path)
-    if loader is None:
+    spec = _find_spec(name, path)
+    if spec is None:
         raise ImportError(_ERR_MSG.format(name), name=name)
-    elif name not in sys.modules:
-        # The parent import may have already imported this module.
-        loader.load_module(name)
-        _verbose_message('import {!r} # {!r}', name, loader)
-    # Backwards-compatibility; be nicer to skip the dict lookup.
-    module = sys.modules[name]
+    else:
+        module = _SpecMethods(spec)._load_unlocked()
     if parent:
         # Set the module as an attribute on its parent.
         parent_module = sys.modules[parent]
         setattr(parent_module, name.rpartition('.')[2], module)
-    # Set __package__ if the loader did not.
-    if getattr(module, '__package__', None) is None:
-        try:
-            module.__package__ = module.__name__
-            if not hasattr(module, '__path__'):
-                module.__package__ = module.__package__.rpartition('.')[0]
-        except AttributeError:
-            pass
-    # Set loader if need be.
-    if getattr(module, '__loader__', None) is None:
-        try:
-            module.__loader__ = loader
-        except AttributeError:
-            pass
     return module
 
 
 def _find_and_load(name, import_):
     """Find and load the module, and release the import lock."""
-    try:
-        lock = _get_module_lock(name)
-    finally:
-        _imp.release_lock()
-    lock.acquire()
-    try:
+    with _ModuleLockManager(name):
         return _find_and_load_unlocked(name, import_)
-    finally:
-        lock.release()
 
 
 def _gcd_import(name, package=None, level=0):
@@ -1733,6 +2266,11 @@
         return _handle_fromlist(module, fromlist, _gcd_import)
 
 
+def _builtin_from_name(name):
+    spec = BuiltinImporter.find_spec(name)
+    methods = _SpecMethods(spec)
+    return methods._load_unlocked()
+
 
 def _setup(sys_module, _imp_module):
     """Setup importlib by importing needed built-in modules and injecting them
@@ -1751,23 +2289,30 @@
     else:
         BYTECODE_SUFFIXES = DEBUG_BYTECODE_SUFFIXES
 
+    # Set up the spec for existing builtin/frozen modules.
     module_type = type(sys)
     for name, module in sys.modules.items():
         if isinstance(module, module_type):
-            if getattr(module, '__loader__', None) is None:
-                if name in sys.builtin_module_names:
-                    module.__loader__ = BuiltinImporter
-                elif _imp.is_frozen(name):
-                    module.__loader__ = FrozenImporter
+            if name in sys.builtin_module_names:
+                loader = BuiltinImporter
+            elif _imp.is_frozen(name):
+                loader = FrozenImporter
+            else:
+                continue
+            spec = _spec_from_module(module, loader)
+            methods = _SpecMethods(spec)
+            methods.init_module_attrs(module)
 
+    # Directly load built-in modules needed during bootstrap.
     self_module = sys.modules[__name__]
     for builtin_name in ('_io', '_warnings', 'builtins', 'marshal'):
         if builtin_name not in sys.modules:
-            builtin_module = BuiltinImporter.load_module(builtin_name)
+            builtin_module = _builtin_from_name(builtin_name)
         else:
             builtin_module = sys.modules[builtin_name]
         setattr(self_module, builtin_name, builtin_module)
 
+    # Directly load the os module (needed during bootstrap).
     os_details = ('posix', ['/']), ('nt', ['\\', '/'])
     for builtin_os, path_separators in os_details:
         # Assumption made in _path_join()
@@ -1778,29 +2323,33 @@
             break
         else:
             try:
-                os_module = BuiltinImporter.load_module(builtin_os)
+                os_module = _builtin_from_name(builtin_os)
                 break
             except ImportError:
                 continue
     else:
         raise ImportError('importlib requires posix or nt')
+    setattr(self_module, '_os', os_module)
+    setattr(self_module, 'path_sep', path_sep)
+    setattr(self_module, 'path_separators', ''.join(path_separators))
 
+    # Directly load the _thread module (needed during bootstrap).
     try:
-        thread_module = BuiltinImporter.load_module('_thread')
+        thread_module = _builtin_from_name('_thread')
     except ImportError:
         # Python was built without threads
         thread_module = None
-    weakref_module = BuiltinImporter.load_module('_weakref')
+    setattr(self_module, '_thread', thread_module)
 
+    # Directly load the _weakref module (needed during bootstrap).
+    weakref_module = _builtin_from_name('_weakref')
+    setattr(self_module, '_weakref', weakref_module)
+
+    # Directly load the winreg module (needed during bootstrap).
     if builtin_os == 'nt':
-        winreg_module = BuiltinImporter.load_module('winreg')
+        winreg_module = _builtin_from_name('winreg')
         setattr(self_module, '_winreg', winreg_module)
 
-    setattr(self_module, '_os', os_module)
-    setattr(self_module, '_thread', thread_module)
-    setattr(self_module, '_weakref', weakref_module)
-    setattr(self_module, 'path_sep', path_sep)
-    setattr(self_module, 'path_separators', ''.join(path_separators))
     # Constants
     setattr(self_module, '_relax_case', _make_relax_case())
     EXTENSION_SUFFIXES.extend(_imp.extension_suffixes())
diff --git a/Lib/importlib/abc.py b/Lib/importlib/abc.py
--- a/Lib/importlib/abc.py
+++ b/Lib/importlib/abc.py
@@ -40,12 +40,18 @@
 
     """Abstract base class for import finders on sys.meta_path."""
 
-    @abc.abstractmethod
+    # We don't define find_spec() here since that would break
+    # hasattr checks we do to support backward compatibility.
+
+    # XXX Deprecate
     def find_module(self, fullname, path):
-        """Abstract method which, when implemented, should find a module.
-        The fullname is a str and the path is a list of strings or None.
-        Returns a Loader object or None.
+        """Return a loader for the module.
+
+        If no module is found, return None.  The fullname is a str and
+        the path is a list of strings or None.
+
         """
+        return None
 
     def invalidate_caches(self):
         """An optional method for clearing the finder's cache, if any.
@@ -60,17 +66,25 @@
 
     """Abstract base class for path entry finders used by PathFinder."""
 
-    @abc.abstractmethod
+    # We don't define find_spec() here since that would break
+    # hasattr checks we do to support backward compatibility.
+
+    # XXX Deprecate.
     def find_loader(self, fullname):
-        """Abstract method which, when implemented, returns a module loader or
-        a possible part of a namespace.
-        The fullname is a str.  Returns a 2-tuple of (Loader, portion) where
-        portion is a sequence of file system locations contributing to part of
-        a namespace package. The sequence may be empty and the loader may be
-        None.
+        """Return (loader, namespace portion) for the path entry.
+
+        The fullname is a str.  The namespace portion is a sequence of
+        path entries contributing to part of a namespace package. The
+        sequence may be empty.  If loader is not None, the portion will
+        be ignored.
+
+        The portion will be discarded if another path entry finder
+        locates the module as a normal module or package.
+
         """
         return None, []
 
+    # XXX Deprecate.
     find_module = _bootstrap._find_module_shim
 
     def invalidate_caches(self):
@@ -83,35 +97,47 @@
 
 class Loader(metaclass=abc.ABCMeta):
 
-    """Abstract base class for import loaders.
+    """Abstract base class for import loaders."""
 
-    The optional method module_repr(module) may be defined to provide a
-    repr for a module when appropriate (see PEP 420). The __repr__() method on
-    the module type will use the method as appropriate.
+    def create_module(self, spec):
+        """Return a module to initialize and into which to load.
 
-    """
+        This method should raise ImportError if anything prevents it
+        from creating a new module.  It may return None to indicate
+        that the spec should create the new module.
 
-    @abc.abstractmethod
+        create_module() is optional.
+
+        """
+        # By default, defer to _SpecMethods.create() for the new module.
+        return None
+
+    # We don't define exec_module() here since that would break
+    # hasattr checks we do to support backward compatibility.
+
+    # XXX Deprecate.
     def load_module(self, fullname):
-        """Abstract method which when implemented should load a module.
-        The fullname is a str.
+        """Return the loaded module.
+
+        The module must be added to sys.modules and have import-related
+        attributes set properly.  The fullname is a str.
 
         ImportError is raised on failure.
+
         """
         raise ImportError
 
+    # XXX Deprecate.
     def module_repr(self, module):
         """Return a module's repr.
 
         Used by the module type when the method does not raise
         NotImplementedError.
+
         """
+        # The exception will cause ModuleType.__repr__ to ignore this method.
         raise NotImplementedError
 
-    def init_module_attrs(self, module):
-        """Set the module's __loader__ attribute."""
-        module.__loader__ = self
-
 
 class ResourceLoader(Loader):
 
@@ -138,12 +164,11 @@
 
     """
 
-    @abc.abstractmethod
     def is_package(self, fullname):
-        """Abstract method which when implemented should return whether the
+        """Optional method which when implemented should return whether the
         module is a package.  The fullname is a str.  Returns a bool.
 
-        Raises ImportError is the module cannot be found.
+        Raises ImportError if the module cannot be found.
         """
         raise ImportError
 
@@ -176,19 +201,10 @@
         argument should be where the data was retrieved (when applicable)."""
         return compile(data, path, 'exec', dont_inherit=True)
 
-    def init_module_attrs(self, module):
-        """Initialize the __loader__ and __package__ attributes of the module.
-
-        The name of the module is gleaned from module.__name__. The __package__
-        attribute is set based on self.is_package().
-        """
-        super().init_module_attrs(module)
-        _bootstrap._init_package_attrs(self, module)
-
+    exec_module = _bootstrap._LoaderBasics.exec_module
     load_module = _bootstrap._LoaderBasics.load_module
 
-_register(InspectLoader, machinery.BuiltinImporter, machinery.FrozenImporter,
-          _bootstrap.NamespaceLoader)
+_register(InspectLoader, machinery.BuiltinImporter, machinery.FrozenImporter)
 
 
 class ExecutionLoader(InspectLoader):
@@ -225,18 +241,6 @@
         else:
             return self.source_to_code(source, path)
 
-    def init_module_attrs(self, module):
-        """Initialize the module's attributes.
-
-        It is assumed that the module's name has been set on module.__name__.
-        It is also assumed that any path returned by self.get_filename() uses
-        (one of) the operating system's path separator(s) to separate filenames
-        from directories in order to set __path__ intelligently.
-        InspectLoader.init_module_attrs() sets __loader__ and __package__.
-        """
-        super().init_module_attrs(module)
-        _bootstrap._init_file_attrs(self, module)
-
 _register(ExecutionLoader, machinery.ExtensionFileLoader)
 
 
diff --git a/Lib/importlib/machinery.py b/Lib/importlib/machinery.py
--- a/Lib/importlib/machinery.py
+++ b/Lib/importlib/machinery.py
@@ -5,6 +5,7 @@
 from ._bootstrap import (SOURCE_SUFFIXES, DEBUG_BYTECODE_SUFFIXES,
                          OPTIMIZED_BYTECODE_SUFFIXES, BYTECODE_SUFFIXES,
                          EXTENSION_SUFFIXES)
+from ._bootstrap import ModuleSpec
 from ._bootstrap import BuiltinImporter
 from ._bootstrap import FrozenImporter
 from ._bootstrap import WindowsRegistryFinder
diff --git a/Lib/importlib/util.py b/Lib/importlib/util.py
--- a/Lib/importlib/util.py
+++ b/Lib/importlib/util.py
@@ -3,13 +3,14 @@
 from ._bootstrap import MAGIC_NUMBER
 from ._bootstrap import cache_from_source
 from ._bootstrap import decode_source
-from ._bootstrap import module_to_load
-from ._bootstrap import set_loader
-from ._bootstrap import set_package
 from ._bootstrap import source_from_cache
+from ._bootstrap import spec_from_loader
+from ._bootstrap import spec_from_file_location
 from ._bootstrap import _resolve_name
 
+from contextlib import contextmanager
 import functools
+import sys
 import warnings
 
 
@@ -28,6 +29,58 @@
     return _resolve_name(name[level:], package, level)
 
 
+ at contextmanager
+def _module_to_load(name):
+    is_reload = name in sys.modules
+
+    module = sys.modules.get(name)
+    if not is_reload:
+        # This must be done before open() is called as the 'io' module
+        # implicitly imports 'locale' and would otherwise trigger an
+        # infinite loop.
+        module = type(sys)(name)
+        # This must be done before putting the module in sys.modules
+        # (otherwise an optimization shortcut in import.c becomes wrong)
+        module.__initializing__ = True
+        sys.modules[name] = module
+    try:
+        yield module
+    except Exception:
+        if not is_reload:
+            try:
+                del sys.modules[name]
+            except KeyError:
+                pass
+    finally:
+        module.__initializing__ = False
+
+
+# XXX deprecate
+def set_package(fxn):
+    """Set __package__ on the returned module."""
+    @functools.wraps(fxn)
+    def set_package_wrapper(*args, **kwargs):
+        module = fxn(*args, **kwargs)
+        if getattr(module, '__package__', None) is None:
+            module.__package__ = module.__name__
+            if not hasattr(module, '__path__'):
+                module.__package__ = module.__package__.rpartition('.')[0]
+        return module
+    return set_package_wrapper
+
+
+# XXX deprecate
+def set_loader(fxn):
+    """Set __loader__ on the returned module."""
+    @functools.wraps(fxn)
+    def set_loader_wrapper(self, *args, **kwargs):
+        module = fxn(self, *args, **kwargs)
+        if getattr(module, '__loader__', None) is None:
+            module.__loader__ = self
+        return module
+    return set_loader_wrapper
+
+
 def module_for_loader(fxn):
     """Decorator to handle selecting the proper module for loaders.
 
@@ -46,13 +99,11 @@
     the second argument.
 
     """
-    warnings.warn('To make it easier for subclasses, please use '
-                  'importlib.util.module_to_load() and '
-                  'importlib.abc.Loader.init_module_attrs()',
+    warnings.warn('The import system now takes care of this automatically.',
                   PendingDeprecationWarning, stacklevel=2)
     @functools.wraps(fxn)
     def module_for_loader_wrapper(self, fullname, *args, **kwargs):
-        with module_to_load(fullname) as module:
+        with _module_to_load(fullname) as module:
             module.__loader__ = self
             try:
                 is_package = self.is_package(fullname)
diff --git a/Lib/multiprocessing/spawn.py b/Lib/multiprocessing/spawn.py
--- a/Lib/multiprocessing/spawn.py
+++ b/Lib/multiprocessing/spawn.py
@@ -245,14 +245,13 @@
         # We should not try to load __main__
         # since that would execute 'if __name__ == "__main__"'
         # clauses, potentially causing a psuedo fork bomb.
-        loader = importlib.find_loader(main_name, path=dirs)
         main_module = types.ModuleType(main_name)
-        try:
-            loader.init_module_attrs(main_module)
-        except AttributeError:  # init_module_attrs is optional
-            pass
+        # XXX Use a target of main_module?
+        spec = importlib.find_spec(main_name, path=dirs)
+        methods = importlib._bootstrap._SpecMethods(spec)
+        methods.init_module_attrs(main_module)
         main_module.__name__ = '__mp_main__'
-        code = loader.get_code(main_name)
+        code = spec.loader.get_code(main_name)
         exec(code, main_module.__dict__)
 
         old_main_modules.append(sys.modules['__main__'])
diff --git a/Lib/pkgutil.py b/Lib/pkgutil.py
--- a/Lib/pkgutil.py
+++ b/Lib/pkgutil.py
@@ -430,6 +430,7 @@
     for item in path:
         yield get_importer(item)
 
+
 def get_loader(module_or_name):
     """Get a PEP 302 "loader" object for module_or_name
 
@@ -570,6 +571,7 @@
 
     return path
 
+
 def get_data(package, resource):
     """Get a resource from a package.
 
@@ -592,10 +594,15 @@
     which does not support get_data(), then None is returned.
     """
 
-    loader = get_loader(package)
+    spec = importlib.find_spec(package)
+    if spec is None:
+        return None
+    loader = spec.loader
     if loader is None or not hasattr(loader, 'get_data'):
         return None
-    mod = sys.modules.get(package) or loader.load_module(package)
+    # XXX needs test
+    mod = (sys.modules.get(package) or
+           importlib._bootstrap._SpecMethods(spec).load())
     if mod is None or not hasattr(mod, '__file__'):
         return None
 
diff --git a/Lib/pydoc.py b/Lib/pydoc.py
--- a/Lib/pydoc.py
+++ b/Lib/pydoc.py
@@ -167,8 +167,9 @@
 def visiblename(name, all=None, obj=None):
     """Decide whether to show documentation on a variable."""
     # Certain special names are redundant or internal.
+    # XXX Remove __initializing__?
     if name in {'__author__', '__builtins__', '__cached__', '__credits__',
-                '__date__', '__doc__', '__file__', '__initializing__',
+                '__date__', '__doc__', '__file__', '__spec__',
                 '__loader__', '__module__', '__name__', '__package__',
                 '__path__', '__qualname__', '__slots__', '__version__'}:
         return 0
diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py
--- a/Lib/test/test_descr.py
+++ b/Lib/test/test_descr.py
@@ -2259,7 +2259,7 @@
         minstance.b = 2
         minstance.a = 1
         default_attributes = ['__name__', '__doc__', '__package__',
-                              '__loader__']
+                              '__loader__', '__spec__']
         names = [x for x in dir(minstance) if x not in default_attributes]
         self.assertEqual(names, ['a', 'b'])
 
diff --git a/Lib/test/test_frozen.py b/Lib/test/test_frozen.py
deleted file mode 100644
--- a/Lib/test/test_frozen.py
+++ /dev/null
@@ -1,77 +0,0 @@
-# Test the frozen module defined in frozen.c.
-
-from test.support import captured_stdout, run_unittest
-import unittest
-import sys
-
-class FrozenTests(unittest.TestCase):
-
-    module_attrs = frozenset(['__builtins__', '__cached__', '__doc__',
-                              '__loader__', '__name__',
-                              '__package__'])
-    package_attrs = frozenset(list(module_attrs) + ['__path__'])
-
-    def test_frozen(self):
-        with captured_stdout() as stdout:
-            try:
-                import __hello__
-            except ImportError as x:
-                self.fail("import __hello__ failed:" + str(x))
-            self.assertEqual(__hello__.initialized, True)
-            expect = set(self.module_attrs)
-            expect.add('initialized')
-            self.assertEqual(set(dir(__hello__)), expect)
-            self.assertEqual(stdout.getvalue(), 'Hello world!\n')
-
-        with captured_stdout() as stdout:
-            try:
-                import __phello__
-            except ImportError as x:
-                self.fail("import __phello__ failed:" + str(x))
-            self.assertEqual(__phello__.initialized, True)
-            expect = set(self.package_attrs)
-            expect.add('initialized')
-            if not "__phello__.spam" in sys.modules:
-                self.assertEqual(set(dir(__phello__)), expect)
-            else:
-                expect.add('spam')
-                self.assertEqual(set(dir(__phello__)), expect)
-            self.assertEqual(__phello__.__path__, [])
-            self.assertEqual(stdout.getvalue(), 'Hello world!\n')
-
-        with captured_stdout() as stdout:
-            try:
-                import __phello__.spam
-            except ImportError as x:
-                self.fail("import __phello__.spam failed:" + str(x))
-            self.assertEqual(__phello__.spam.initialized, True)
-            spam_expect = set(self.module_attrs)
-            spam_expect.add('initialized')
-            self.assertEqual(set(dir(__phello__.spam)), spam_expect)
-            phello_expect = set(self.package_attrs)
-            phello_expect.add('initialized')
-            phello_expect.add('spam')
-            self.assertEqual(set(dir(__phello__)), phello_expect)
-            self.assertEqual(stdout.getvalue(), 'Hello world!\n')
-
-        try:
-            import __phello__.foo
-        except ImportError:
-            pass
-        else:
-            self.fail("import __phello__.foo should have failed")
-
-            try:
-                import __phello__.foo
-            except ImportError:
-                pass
-            else:
-                self.fail("import __phello__.foo should have failed")
-
-        del sys.modules['__hello__']
-        del sys.modules['__phello__']
-        del sys.modules['__phello__.spam']
-
-
-if __name__ == "__main__":
-    unittest.main()
diff --git a/Lib/test/test_import.py b/Lib/test/test_import.py
--- a/Lib/test/test_import.py
+++ b/Lib/test/test_import.py
@@ -1036,11 +1036,14 @@
         # away from the traceback.
         self.create_module("foo", "")
         importlib = sys.modules['_frozen_importlib']
-        old_load_module = importlib.SourceLoader.load_module
+        if 'load_module' in vars(importlib.SourceLoader):
+            old_exec_module = importlib.SourceLoader.exec_module
+        else:
+            old_exec_module = None
         try:
-            def load_module(*args):
+            def exec_module(*args):
                 1/0
-            importlib.SourceLoader.load_module = load_module
+            importlib.SourceLoader.exec_module = exec_module
             try:
                 import foo
             except ZeroDivisionError as e:
@@ -1049,7 +1052,10 @@
                 self.fail("ZeroDivisionError should have been raised")
             self.assert_traceback(tb, [__file__, '<frozen importlib', __file__])
         finally:
-            importlib.SourceLoader.load_module = old_load_module
+            if old_exec_module is None:
+                del importlib.SourceLoader.exec_module
+            else:
+                importlib.SourceLoader.exec_module = old_exec_module
 
     @unittest.skipUnless(TESTFN_UNENCODABLE, 'need TESTFN_UNENCODABLE')
     def test_unencodable_filename(self):
diff --git a/Lib/test/test_importlib/abc.py b/Lib/test/test_importlib/abc.py
--- a/Lib/test/test_importlib/abc.py
+++ b/Lib/test/test_importlib/abc.py
@@ -81,11 +81,6 @@
         pass
 
     @abc.abstractmethod
-    def test_module_reuse(self):
-        """If a module is already in sys.modules, it should be reused."""
-        pass
-
-    @abc.abstractmethod
     def test_state_after_failure(self):
         """If a module is already in sys.modules and a reload fails
         (e.g. a SyntaxError), the module should be in the state it was before
diff --git a/Lib/test/test_importlib/builtin/test_finder.py b/Lib/test/test_importlib/builtin/test_finder.py
--- a/Lib/test/test_importlib/builtin/test_finder.py
+++ b/Lib/test/test_importlib/builtin/test_finder.py
@@ -8,6 +8,46 @@
 import unittest
 
 
+class FindSpecTests(abc.FinderTests):
+
+    """Test find_spec() for built-in modules."""
+
+    def test_module(self):
+        # Common case.
+        with util.uncache(builtin_util.NAME):
+            found = self.machinery.BuiltinImporter.find_spec(builtin_util.NAME)
+            self.assertTrue(found)
+            self.assertEqual(found.origin, 'built-in')
+
+    # Built-in modules cannot be a package.
+    test_package = None
+
+    # Built-in modules cannobt be in a package.
+    test_module_in_package = None
+
+    # Built-in modules cannot be a package.
+    test_package_in_package = None
+
+    # Built-in modules cannot be a package.
+    test_package_over_module = None
+
+    def test_failure(self):
+        name = 'importlib'
+        assert name not in sys.builtin_module_names
+        spec = self.machinery.BuiltinImporter.find_spec(name)
+        self.assertIsNone(spec)
+
+    def test_ignore_path(self):
+        # The value for 'path' should always trigger a failed import.
+        with util.uncache(builtin_util.NAME):
+            spec = self.machinery.BuiltinImporter.find_spec(builtin_util.NAME,
+                                                            ['pkg'])
+            self.assertIsNone(spec)
+
+Frozen_FindSpecTests, Source_FindSpecTests = util.test_both(FindSpecTests,
+        machinery=[frozen_machinery, source_machinery])
+
+
 class FinderTests(abc.FinderTests):
 
     """Test find_module() for built-in modules."""
@@ -17,22 +57,13 @@
         with util.uncache(builtin_util.NAME):
             found = self.machinery.BuiltinImporter.find_module(builtin_util.NAME)
             self.assertTrue(found)
+            self.assertTrue(hasattr(found, 'load_module'))
 
-    def test_package(self):
-        # Built-in modules cannot be a package.
-        pass
+    # Built-in modules cannot be a package.
+    test_package = test_package_in_package = test_package_over_module = None
 
-    def test_module_in_package(self):
-        # Built-in modules cannobt be in a package.
-        pass
-
-    def test_package_in_package(self):
-        # Built-in modules cannot be a package.
-        pass
-
-    def test_package_over_module(self):
-        # Built-in modules cannot be a package.
-        pass
+    # Built-in modules cannot be in a package.
+    test_module_in_package = None
 
     def test_failure(self):
         assert 'importlib' not in sys.builtin_module_names
diff --git a/Lib/test/test_importlib/builtin/test_loader.py b/Lib/test/test_importlib/builtin/test_loader.py
--- a/Lib/test/test_importlib/builtin/test_loader.py
+++ b/Lib/test/test_importlib/builtin/test_loader.py
@@ -9,6 +9,70 @@
 import unittest
 
 
+class ExecModTests(abc.LoaderTests):
+
+    """Test exec_module() for built-in modules."""
+
+    @classmethod
+    def setUpClass(cls):
+        cls.verification = {'__name__': 'errno', '__package__': '',
+                             '__loader__': cls.machinery.BuiltinImporter}
+
+    def verify(self, module):
+        """Verify that the module matches against what it should have."""
+        self.assertIsInstance(module, types.ModuleType)
+        for attr, value in self.verification.items():
+            self.assertEqual(getattr(module, attr), value)
+        self.assertIn(module.__name__, sys.modules)
+        self.assertTrue(hasattr(module, '__spec__'))
+        self.assertEqual(module.__spec__.origin, 'built-in')
+
+    def load_spec(self, name):
+        spec = self.machinery.ModuleSpec(name, self.machinery.BuiltinImporter,
+                                         origin='built-in')
+        module = types.ModuleType(name)
+        module.__spec__ = spec
+        self.machinery.BuiltinImporter.exec_module(module)
+        # Strictly not what exec_module() is supposed to do, but since
+        # _imp.init_builtin() does this we can't get around it.
+        return sys.modules[name]
+
+    def test_module(self):
+        # Common case.
+        with util.uncache(builtin_util.NAME):
+            module = self.load_spec(builtin_util.NAME)
+            self.verify(module)
+            self.assertIn('built-in', str(module))
+
+    # Built-in modules cannot be a package.
+    test_package = None
+
+    # Built-in modules cannot be a package.
+    test_lacking_parent = None
+
+    # Not way to force an import failure.
+    test_state_after_failure = None
+
+    def test_unloadable(self):
+        name = 'dssdsdfff'
+        assert name not in sys.builtin_module_names
+        with self.assertRaises(ImportError) as cm:
+            self.load_spec(name)
+        self.assertEqual(cm.exception.name, name)
+
+    def test_already_imported(self):
+        # Using the name of a module already imported but not a built-in should
+        # still fail.
+        assert hasattr(unittest, '__file__')  # Not a built-in.
+        with self.assertRaises(ImportError) as cm:
+            self.load_spec('unittest')
+        self.assertEqual(cm.exception.name, 'unittest')
+
+
+Frozen_ExecModTests, Source_ExecModTests = util.test_both(ExecModTests,
+        machinery=[frozen_machinery, source_machinery])
+
+
 class LoaderTests(abc.LoaderTests):
 
     """Test load_module() for built-in modules."""
@@ -33,17 +97,11 @@
             module = self.load_module(builtin_util.NAME)
             self.verify(module)
 
-    def test_package(self):
-        # Built-in modules cannot be a package.
-        pass
+    # Built-in modules cannot be a package.
+    test_package = test_lacking_parent = None
 
-    def test_lacking_parent(self):
-        # Built-in modules cannot be a package.
-        pass
-
-    def test_state_after_failure(self):
-        # Not way to force an imoprt failure.
-        pass
+    # No way to force an import failure.
+    test_state_after_failure = None
 
     def test_module_reuse(self):
         # Test that the same module is used in a reload.
diff --git a/Lib/test/test_importlib/extension/test_case_sensitivity.py b/Lib/test/test_importlib/extension/test_case_sensitivity.py
--- a/Lib/test/test_importlib/extension/test_case_sensitivity.py
+++ b/Lib/test/test_importlib/extension/test_case_sensitivity.py
@@ -9,6 +9,8 @@
 frozen_machinery, source_machinery = util.import_importlib('importlib.machinery')
 
 
+# XXX find_spec tests
+
 @unittest.skipIf(ext_util.FILENAME is None, '_testcapi not available')
 @util.case_insensitive_tests
 class ExtensionModuleCaseSensitivityTest:
diff --git a/Lib/test/test_importlib/extension/test_finder.py b/Lib/test/test_importlib/extension/test_finder.py
--- a/Lib/test/test_importlib/extension/test_finder.py
+++ b/Lib/test/test_importlib/extension/test_finder.py
@@ -6,6 +6,7 @@
 
 import unittest
 
+# XXX find_spec tests
 
 class FinderTests(abc.FinderTests):
 
@@ -20,21 +21,14 @@
     def test_module(self):
         self.assertTrue(self.find_module(util.NAME))
 
-    def test_package(self):
-        # No extension module as an __init__ available for testing.
-        pass
+    # No extension module as an __init__ available for testing.
+    test_package = test_package_in_package = None
 
-    def test_module_in_package(self):
-        # No extension module in a package available for testing.
-        pass
+    # No extension module in a package available for testing.
+    test_module_in_package = None
 
-    def test_package_in_package(self):
-        # No extension module as an __init__ available for testing.
-        pass
-
-    def test_package_over_module(self):
-        # Extension modules cannot be an __init__ for a package.
-        pass
+    # Extension modules cannot be an __init__ for a package.
+    test_package_over_module = None
 
     def test_failure(self):
         self.assertIsNone(self.find_module('asdfjkl;'))
diff --git a/Lib/test/test_importlib/extension/test_loader.py b/Lib/test/test_importlib/extension/test_loader.py
--- a/Lib/test/test_importlib/extension/test_loader.py
+++ b/Lib/test/test_importlib/extension/test_loader.py
@@ -6,9 +6,75 @@
 
 import os.path
 import sys
+import types
 import unittest
 
 
+class ExecModuleTests(abc.LoaderTests):
+
+    """Test load_module() for extension modules."""
+
+    def setUp(self):
+        self.loader = self.machinery.ExtensionFileLoader(ext_util.NAME,
+                                                         ext_util.FILEPATH)
+
+    def exec_module(self, fullname):
+        module = types.ModuleType(fullname)
+        module.__spec__ = self.machinery.ModuleSpec(fullname, self.loader)
+        self.loader.exec_module(module)
+        return sys.modules[fullname]
+
+    def test_exec_module_API(self):
+        with self.assertRaises(ImportError):
+            self.exec_module('XXX')
+
+
+    def test_module(self):
+        with util.uncache(ext_util.NAME):
+            module = self.exec_module(ext_util.NAME)
+            for attr, value in [('__name__', ext_util.NAME),
+                                ('__file__', ext_util.FILEPATH),
+                                ('__package__', '')]:
+                given = getattr(module, attr)
+                self.assertEqual(given, value,
+                                 '{}: {!r} != {!r}'.format(attr, given, value))
+            self.assertIn(ext_util.NAME, sys.modules)
+            self.assertIsInstance(module.__loader__,
+                                  self.machinery.ExtensionFileLoader)
+
+    # No extension module as __init__ available for testing.
+    test_package = None
+
+    # No extension module in a package available for testing.
+    test_lacking_parent = None
+
+    def test_module_reuse(self):
+        with util.uncache(ext_util.NAME):
+            module1 = self.exec_module(ext_util.NAME)
+            module2 = self.exec_module(ext_util.NAME)
+            self.assertIs(module1, module2)
+
+    def test_state_after_failure(self):
+        # No easy way to trigger a failure after a successful import.
+        pass
+
+    def test_unloadable(self):
+        name = 'asdfjkl;'
+        with self.assertRaises(ImportError) as cm:
+            self.exec_module(name)
+        self.assertEqual(cm.exception.name, name)
+
+    def test_is_package(self):
+        self.assertFalse(self.loader.is_package(ext_util.NAME))
+        for suffix in self.machinery.EXTENSION_SUFFIXES:
+            path = os.path.join('some', 'path', 'pkg', '__init__' + suffix)
+            loader = self.machinery.ExtensionFileLoader('pkg', path)
+            self.assertTrue(loader.is_package('pkg'))
+
+Frozen_ExecModuleTests, Source_ExecModuleTests = util.test_both(
+        ExecModuleTests, machinery=machinery)
+
+
 class LoaderTests(abc.LoaderTests):
 
     """Test load_module() for extension modules."""
@@ -39,13 +105,11 @@
             self.assertIsInstance(module.__loader__,
                                   self.machinery.ExtensionFileLoader)
 
-    def test_package(self):
-        # No extension module as __init__ available for testing.
-        pass
+    # No extension module as __init__ available for testing.
+    test_package = None
 
-    def test_lacking_parent(self):
-        # No extension module in a package available for testing.
-        pass
+    # No extension module in a package available for testing.
+    test_lacking_parent = None
 
     def test_module_reuse(self):
         with util.uncache(ext_util.NAME):
@@ -53,9 +117,8 @@
             module2 = self.load_module(ext_util.NAME)
             self.assertIs(module1, module2)
 
-    def test_state_after_failure(self):
-        # No easy way to trigger a failure after a successful import.
-        pass
+    # No easy way to trigger a failure after a successful import.
+    test_state_after_failure = None
 
     def test_unloadable(self):
         name = 'asdfjkl;'
diff --git a/Lib/test/test_importlib/frozen/test_finder.py b/Lib/test/test_importlib/frozen/test_finder.py
--- a/Lib/test/test_importlib/frozen/test_finder.py
+++ b/Lib/test/test_importlib/frozen/test_finder.py
@@ -6,6 +6,41 @@
 import unittest
 
 
+class FindSpecTests(abc.FinderTests):
+
+    """Test finding frozen modules."""
+
+    def find(self, name, path=None):
+        finder = self.machinery.FrozenImporter
+        return finder.find_spec(name, path)
+
+    def test_module(self):
+        name = '__hello__'
+        spec = self.find(name)
+        self.assertEqual(spec.origin, 'frozen')
+
+    def test_package(self):
+        spec = self.find('__phello__')
+        self.assertIsNotNone(spec)
+
+    def test_module_in_package(self):
+        spec = self.find('__phello__.spam', ['__phello__'])
+        self.assertIsNotNone(spec)
+
+    # No frozen package within another package to test with.
+    test_package_in_package = None
+
+    # No easy way to test.
+    test_package_over_module = None
+
+    def test_failure(self):
+        spec = self.find('<not real>')
+        self.assertIsNone(spec)
+
+Frozen_FindSpecTests, Source_FindSpecTests = util.test_both(FindSpecTests,
+                                                            machinery=machinery)
+
+
 class FinderTests(abc.FinderTests):
 
     """Test finding frozen modules."""
@@ -27,13 +62,11 @@
         loader = self.find('__phello__.spam', ['__phello__'])
         self.assertTrue(hasattr(loader, 'load_module'))
 
-    def test_package_in_package(self):
-        # No frozen package within another package to test with.
-        pass
+    # No frozen package within another package to test with.
+    test_package_in_package = None
 
-    def test_package_over_module(self):
-        # No easy way to test.
-        pass
+    # No easy way to test.
+    test_package_over_module = None
 
     def test_failure(self):
         loader = self.find('<not real>')
diff --git a/Lib/test/test_importlib/frozen/test_loader.py b/Lib/test/test_importlib/frozen/test_loader.py
--- a/Lib/test/test_importlib/frozen/test_loader.py
+++ b/Lib/test/test_importlib/frozen/test_loader.py
@@ -3,9 +3,81 @@
 
 machinery = util.import_importlib('importlib.machinery')
 
-import unittest
+
+import sys
 from test.support import captured_stdout
 import types
+import unittest
+
+
+class ExecModuleTests(abc.LoaderTests):
+
+    def exec_module(self, name):
+        with util.uncache(name), captured_stdout() as stdout:
+            spec = self.machinery.ModuleSpec(
+                    name, self.machinery.FrozenImporter, origin='frozen',
+                    is_package=self.machinery.FrozenImporter.is_package(name))
+            module = types.ModuleType(name)
+            module.__spec__ = spec
+            assert not hasattr(module, 'initialized')
+            self.machinery.FrozenImporter.exec_module(module)
+            self.assertTrue(module.initialized)
+            self.assertTrue(hasattr(module, '__spec__'))
+            self.assertEqual(module.__spec__.origin, 'frozen')
+            return module, stdout.getvalue()
+
+    def test_module(self):
+        name = '__hello__'
+        module, output = self.exec_module(name)
+        check = {'__name__': name}
+        for attr, value in check.items():
+            self.assertEqual(getattr(module, attr), value)
+        self.assertEqual(output, 'Hello world!\n')
+        self.assertTrue(hasattr(module, '__spec__'))
+
+    def test_package(self):
+        name = '__phello__'
+        module, output = self.exec_module(name)
+        check = {'__name__': name}
+        for attr, value in check.items():
+            attr_value = getattr(module, attr)
+            self.assertEqual(attr_value, value,
+                        'for {name}.{attr}, {given!r} != {expected!r}'.format(
+                                 name=name, attr=attr, given=attr_value,
+                                 expected=value))
+        self.assertEqual(output, 'Hello world!\n')
+
+    def test_lacking_parent(self):
+        name = '__phello__.spam'
+        with util.uncache('__phello__'):
+            module, output = self.exec_module(name)
+            check = {'__name__': name}
+            for attr, value in check.items():
+                attr_value = getattr(module, attr)
+                self.assertEqual(attr_value, value,
+                        'for {name}.{attr}, {given} != {expected!r}'.format(
+                                 name=name, attr=attr, given=attr_value,
+                                 expected=value))
+            self.assertEqual(output, 'Hello world!\n')
+
+
+    def test_module_repr(self):
+        name = '__hello__'
+        module, output = self.exec_module(name)
+        self.assertEqual(repr(module),
+                         "<module '__hello__' (frozen)>")
+
+    # No way to trigger an error in a frozen module.
+    test_state_after_failure = None
+
+    def test_unloadable(self):
+        assert self.machinery.FrozenImporter.find_module('_not_real') is None
+        with self.assertRaises(ImportError) as cm:
+            self.exec_module('_not_real')
+        self.assertEqual(cm.exception.name, '_not_real')
+
+Frozen_ExecModuleTests, Source_ExecModuleTests = util.test_both(ExecModuleTests,
+                                                        machinery=machinery)
 
 
 class LoaderTests(abc.LoaderTests):
@@ -68,9 +140,8 @@
             self.assertEqual(repr(module),
                              "<module '__hello__' (frozen)>")
 
-    def test_state_after_failure(self):
-        # No way to trigger an error in a frozen module.
-        pass
+    # No way to trigger an error in a frozen module.
+    test_state_after_failure = None
 
     def test_unloadable(self):
         assert self.machinery.FrozenImporter.find_module('_not_real') is None
diff --git a/Lib/test/test_importlib/import_/test_meta_path.py b/Lib/test/test_importlib/import_/test_meta_path.py
--- a/Lib/test/test_importlib/import_/test_meta_path.py
+++ b/Lib/test/test_importlib/import_/test_meta_path.py
@@ -46,8 +46,8 @@
         with util.import_state(meta_path=[]):
             with warnings.catch_warnings(record=True) as w:
                 warnings.simplefilter('always')
-                self.assertIsNone(importlib._bootstrap._find_module('nothing',
-                                                                    None))
+                self.assertIsNone(importlib._bootstrap._find_spec('nothing',
+                                                                  None))
                 self.assertEqual(len(w), 1)
                 self.assertTrue(issubclass(w[-1].category, ImportWarning))
 
diff --git a/Lib/test/test_importlib/test_abc.py b/Lib/test/test_importlib/test_abc.py
--- a/Lib/test/test_importlib/test_abc.py
+++ b/Lib/test/test_importlib/test_abc.py
@@ -284,22 +284,6 @@
 tests = make_return_value_tests(ExecutionLoader, InspectLoaderDefaultsTests)
 Frozen_ELDefaultTests, Source_ELDefaultsTests = tests
 
-##### Loader concrete methods ##################################################
-class LoaderConcreteMethodTests:
-
-    def test_init_module_attrs(self):
-        loader = self.LoaderSubclass()
-        module = types.ModuleType('blah')
-        loader.init_module_attrs(module)
-        self.assertEqual(module.__loader__, loader)
-
-
-class Frozen_LoaderConcreateMethodTests(LoaderConcreteMethodTests, unittest.TestCase):
-    LoaderSubclass = Frozen_L
-
-class Source_LoaderConcreateMethodTests(LoaderConcreteMethodTests, unittest.TestCase):
-    LoaderSubclass = Source_L
-
 
 ##### InspectLoader concrete methods ###########################################
 class InspectLoaderSourceToCodeTests:
@@ -385,60 +369,6 @@
     InspectLoaderSubclass = Source_IL
 
 
-class InspectLoaderInitModuleTests:
-
-    def mock_is_package(self, return_value):
-        return mock.patch.object(self.InspectLoaderSubclass, 'is_package',
-                                 return_value=return_value)
-
-    def init_module_attrs(self, name):
-        loader = self.InspectLoaderSubclass()
-        module = types.ModuleType(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 Frozen_ILInitModuleTests(InspectLoaderInitModuleTests, unittest.TestCase):
-    InspectLoaderSubclass = Frozen_IL
-
-class Source_ILInitModuleTests(InspectLoaderInitModuleTests, unittest.TestCase):
-    InspectLoaderSubclass = Source_IL
-
-
 class InspectLoaderLoadModuleTests:
 
     """Test InspectLoader.load_module()."""
@@ -550,80 +480,6 @@
     ExecutionLoaderSubclass = Source_EL
 
 
-class ExecutionLoaderInitModuleTests:
-
-    def mock_is_package(self, return_value):
-        return mock.patch.object(self.ExecutionLoaderSubclass, 'is_package',
-                                 return_value=return_value)
-
-    @contextlib.contextmanager
-    def mock_methods(self, is_package, filename):
-        is_package_manager = self.mock_is_package(is_package)
-        get_filename_manager = mock.patch.object(self.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 = self.ExecutionLoaderSubclass()
-            module = types.ModuleType(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 = self.ExecutionLoaderSubclass()
-            module = types.ModuleType(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 = self.ExecutionLoaderSubclass()
-            module = types.ModuleType(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 = self.ExecutionLoaderSubclass()
-            module = types.ModuleType(name)
-            loader.init_module_attrs(module)
-        self.assertFalse(hasattr(module, '__file__'))
-
-
-class Frozen_ELInitModuleTests(ExecutionLoaderInitModuleTests, unittest.TestCase):
-    ExecutionLoaderSubclass = Frozen_EL
-
-class Source_ELInitModuleTests(ExecutionLoaderInitModuleTests, unittest.TestCase):
-    ExecutionLoaderSubclass = Source_EL
-
-
 ##### SourceLoader concrete methods ############################################
 class SourceLoader:
 
@@ -952,58 +808,5 @@
     SourceOnlyLoaderMock = Source_SourceOnlyL
 
 
-class SourceLoaderInitModuleAttrTests:
-
-    """Tests for importlib.abc.SourceLoader.init_module_attrs()."""
-
-    def test_init_module_attrs(self):
-        # If __file__ set, __cached__ == importlib.util.cached_from_source(__file__).
-        name = 'blah'
-        path = 'blah.py'
-        loader = self.SourceOnlyLoaderMock(path)
-        module = types.ModuleType(name)
-        loader.init_module_attrs(module)
-        self.assertEqual(module.__loader__, loader)
-        self.assertEqual(module.__package__, '')
-        self.assertEqual(module.__file__, path)
-        self.assertEqual(module.__cached__, self.util.cache_from_source(path))
-
-    def test_no_get_filename(self):
-        # No __file__, no __cached__.
-        with mock.patch.object(self.SourceOnlyLoaderMock, 'get_filename') as mocked:
-            mocked.side_effect = ImportError
-            name = 'blah'
-            loader = self.SourceOnlyLoaderMock('blah.py')
-            module = types.ModuleType(name)
-            loader.init_module_attrs(module)
-        self.assertFalse(hasattr(module, '__file__'))
-        self.assertFalse(hasattr(module, '__cached__'))
-
-
-class Frozen_SLInitModuleAttrTests(SourceLoaderInitModuleAttrTests, unittest.TestCase):
-    SourceOnlyLoaderMock = Frozen_SourceOnlyL
-    util = frozen_util
-
-    # Difficult to test under source thanks to cross-module mocking needs.
-    @mock.patch('importlib._bootstrap.cache_from_source')
-    def test_cache_from_source_NotImplementedError(self, mock_cache_from_source):
-        # If importlib.util.cache_from_source() raises NotImplementedError don't set
-        # __cached__.
-        mock_cache_from_source.side_effect = NotImplementedError
-        name = 'blah'
-        path = 'blah.py'
-        loader = self.SourceOnlyLoaderMock(path)
-        module = types.ModuleType(name)
-        loader.init_module_attrs(module)
-        self.assertEqual(module.__file__, path)
-        self.assertFalse(hasattr(module, '__cached__'))
-
-
-class Source_SLInitModuleAttrTests(SourceLoaderInitModuleAttrTests, unittest.TestCase):
-    SourceOnlyLoaderMock = Source_SourceOnlyL
-    util = source_util
-
-
-
 if __name__ == '__main__':
     unittest.main()
diff --git a/Lib/test/test_importlib/test_api.py b/Lib/test/test_importlib/test_api.py
--- a/Lib/test/test_importlib/test_api.py
+++ b/Lib/test/test_importlib/test_api.py
@@ -165,6 +165,96 @@
     init = source_init
 
 
+class FindSpecTests:
+
+    class FakeMetaFinder:
+        @staticmethod
+        def find_spec(name, path=None, target=None): return name, path, target
+
+    def test_sys_modules(self):
+        name = 'some_mod'
+        with util.uncache(name):
+            module = types.ModuleType(name)
+            loader = 'a loader!'
+            spec = self.machinery.ModuleSpec(name, loader)
+            module.__loader__ = loader
+            module.__spec__ = spec
+            sys.modules[name] = module
+            found = self.init.find_spec(name)
+            self.assertEqual(found, spec)
+
+    def test_sys_modules_without___loader__(self):
+        name = 'some_mod'
+        with util.uncache(name):
+            module = types.ModuleType(name)
+            del module.__loader__
+            loader = 'a loader!'
+            spec = self.machinery.ModuleSpec(name, loader)
+            module.__spec__ = spec
+            sys.modules[name] = module
+            found = self.init.find_spec(name)
+            self.assertEqual(found, spec)
+
+    def test_sys_modules_spec_is_None(self):
+        name = 'some_mod'
+        with util.uncache(name):
+            module = types.ModuleType(name)
+            module.__spec__ = None
+            sys.modules[name] = module
+            with self.assertRaises(ValueError):
+                self.init.find_spec(name)
+
+    def test_sys_modules_loader_is_None(self):
+        name = 'some_mod'
+        with util.uncache(name):
+            module = types.ModuleType(name)
+            spec = self.machinery.ModuleSpec(name, None)
+            module.__spec__ = spec
+            sys.modules[name] = module
+            found = self.init.find_spec(name)
+            self.assertEqual(found, spec)
+
+    def test_sys_modules_spec_is_not_set(self):
+        name = 'some_mod'
+        with util.uncache(name):
+            module = types.ModuleType(name)
+            try:
+                del module.__spec__
+            except AttributeError:
+                pass
+            sys.modules[name] = module
+            with self.assertRaises(ValueError):
+                self.init.find_spec(name)
+
+    def test_success(self):
+        name = 'some_mod'
+        with util.uncache(name):
+            with util.import_state(meta_path=[self.FakeMetaFinder]):
+                self.assertEqual((name, None, None),
+                                 self.init.find_spec(name))
+
+    def test_success_path(self):
+        # Searching on a path should work.
+        name = 'some_mod'
+        path = 'path to some place'
+        with util.uncache(name):
+            with util.import_state(meta_path=[self.FakeMetaFinder]):
+                self.assertEqual((name, path, None),
+                                 self.init.find_spec(name, path))
+
+    def test_nothing(self):
+        # None is returned upon failure to find a loader.
+        self.assertIsNone(self.init.find_spec('nevergoingtofindthismodule'))
+
+class Frozen_FindSpecTests(FindSpecTests, unittest.TestCase):
+    init = frozen_init
+    machinery = frozen_machinery
+
+class Source_FindSpecTests(FindSpecTests, unittest.TestCase):
+    init = source_init
+    machinery = source_machinery
+
+
 class ReloadTests:
 
     """Test module reloading for builtin and extension modules."""
@@ -219,6 +309,7 @@
         with support.temp_cwd(None) as cwd:
             with util.uncache('spam'):
                 with support.DirsOnSysPath(cwd):
+                    # Start as a plain module.
                     self.init.invalidate_caches()
                     path = os.path.join(cwd, name + '.py')
                     cached = self.util.cache_from_source(path)
@@ -232,11 +323,14 @@
                     support.create_empty_file(path)
                     module = self.init.import_module(name)
                     ns = vars(module)
-                    del ns['__initializing__']
                     loader = ns.pop('__loader__')
+                    spec = ns.pop('__spec__')
+                    self.assertEqual(spec.name, name)
+                    self.assertEqual(spec.loader, loader)
                     self.assertEqual(loader.path, path)
                     self.assertEqual(ns, expected)
 
+                    # Change to a package.
                     self.init.invalidate_caches()
                     init_path = os.path.join(cwd, name, '__init__.py')
                     cached = self.util.cache_from_source(init_path)
@@ -252,18 +346,21 @@
                     os.rename(path, init_path)
                     reloaded = self.init.reload(module)
                     ns = vars(reloaded)
-                    del ns['__initializing__']
                     loader = ns.pop('__loader__')
+                    spec = ns.pop('__spec__')
+                    self.assertEqual(spec.name, name)
+                    self.assertEqual(spec.loader, loader)
                     self.assertIs(reloaded, module)
                     self.assertEqual(loader.path, init_path)
+                    self.maxDiff = None
                     self.assertEqual(ns, expected)
 
     def test_reload_namespace_changed(self):
-        self.maxDiff = None
         name = 'spam'
         with support.temp_cwd(None) as cwd:
             with util.uncache('spam'):
                 with support.DirsOnSysPath(cwd):
+                    # Start as a namespace package.
                     self.init.invalidate_caches()
                     bad_path = os.path.join(cwd, name, '__init.py')
                     cached = self.util.cache_from_source(bad_path)
@@ -276,9 +373,12 @@
                         init_file.write('eggs = None')
                     module = self.init.import_module(name)
                     ns = vars(module)
-                    del ns['__initializing__']
                     loader = ns.pop('__loader__')
                     path = ns.pop('__path__')
+                    spec = ns.pop('__spec__')
+                    self.assertEqual(spec.name, name)
+                    self.assertIs(spec.loader, None)
+                    self.assertIsNot(loader, None)
                     self.assertEqual(set(path),
                                      set([os.path.dirname(bad_path)]))
                     with self.assertRaises(AttributeError):
@@ -286,6 +386,7 @@
                         loader.path
                     self.assertEqual(ns, expected)
 
+                    # Change to a regular package.
                     self.init.invalidate_caches()
                     init_path = os.path.join(cwd, name, '__init__.py')
                     cached = self.util.cache_from_source(init_path)
@@ -301,8 +402,10 @@
                     os.rename(bad_path, init_path)
                     reloaded = self.init.reload(module)
                     ns = vars(reloaded)
-                    del ns['__initializing__']
                     loader = ns.pop('__loader__')
+                    spec = ns.pop('__spec__')
+                    self.assertEqual(spec.name, name)
+                    self.assertEqual(spec.loader, loader)
                     self.assertIs(reloaded, module)
                     self.assertEqual(loader.path, init_path)
                     self.assertEqual(ns, expected)
@@ -371,12 +474,23 @@
         # Issue #17098: all modules should have __loader__ defined.
         for name, module in sys.modules.items():
             if isinstance(module, types.ModuleType):
-                self.assertTrue(hasattr(module, '__loader__'),
-                                '{!r} lacks a __loader__ attribute'.format(name))
-                if self.machinery.BuiltinImporter.find_module(name):
-                    self.assertIsNot(module.__loader__, None)
-                elif self.machinery.FrozenImporter.find_module(name):
-                    self.assertIsNot(module.__loader__, None)
+                with self.subTest(name=name):
+                    self.assertTrue(hasattr(module, '__loader__'),
+                                    '{!r} lacks a __loader__ attribute'.format(name))
+                    if self.machinery.BuiltinImporter.find_module(name):
+                        self.assertIsNot(module.__loader__, None)
+                    elif self.machinery.FrozenImporter.find_module(name):
+                        self.assertIsNot(module.__loader__, None)
+
+    def test_everyone_has___spec__(self):
+        for name, module in sys.modules.items():
+            if isinstance(module, types.ModuleType):
+                with self.subTest(name=name):
+                    self.assertTrue(hasattr(module, '__spec__'))
+                    if self.machinery.BuiltinImporter.find_module(name):
+                        self.assertIsNot(module.__spec__, None)
+                    elif self.machinery.FrozenImporter.find_module(name):
+                        self.assertIsNot(module.__spec__, None)
 
 class Frozen_StartupTests(StartupTests, unittest.TestCase):
     machinery = frozen_machinery
diff --git a/Lib/test/test_importlib/test_spec.py b/Lib/test/test_importlib/test_spec.py
new file mode 100644
--- /dev/null
+++ b/Lib/test/test_importlib/test_spec.py
@@ -0,0 +1,968 @@
+from . import util
+
+frozen_init, source_init = util.import_importlib('importlib')
+frozen_bootstrap = frozen_init._bootstrap
+source_bootstrap = source_init._bootstrap
+frozen_machinery, source_machinery = util.import_importlib('importlib.machinery')
+frozen_util, source_util = util.import_importlib('importlib.util')
+
+import os.path
+from test.support import CleanImport
+import unittest
+import sys
+
+
+
+class TestLoader:
+
+    def __init__(self, path=None, is_package=None):
+#        if path:
+#            if is_package:
+#                if not path.endswith('.py'):
+#                    path = os.path.join(path, '__init__.py')
+#            elif is_package is None:
+#                is_package = path.endswith('__init__.py')
+
+        self.path = path
+        self.package = is_package
+
+    def __repr__(self):
+        return '<TestLoader object>'
+
+    def __getattr__(self, name):
+        if name == 'get_filename' and self.path is not None:
+            return self._get_filename
+        if name == 'is_package':
+            return self._is_package
+        raise AttributeError(name)
+
+    def _get_filename(self, name):
+        return self.path
+
+    def _is_package(self, name):
+        return self.package
+
+
+class NewLoader(TestLoader):
+
+    EGGS = 1
+
+    def exec_module(self, module):
+        module.eggs = self.EGGS
+
+
+class LegacyLoader(TestLoader):
+
+    HAM = -1
+
+    @frozen_util.module_for_loader
+    def load_module(self, module):
+        module.ham = self.HAM
+        return module
+
+
+class ModuleSpecTests:
+
+    def setUp(self):
+        self.name = 'spam'
+        self.path = 'spam.py'
+        self.cached = self.util.cache_from_source(self.path)
+        self.loader = TestLoader()
+        self.spec = self.machinery.ModuleSpec(self.name, self.loader)
+        self.loc_spec = self.machinery.ModuleSpec(self.name, self.loader,
+                                                  origin=self.path)
+        self.loc_spec._set_fileattr = True
+
+    def test_default(self):
+        spec = self.machinery.ModuleSpec(self.name, self.loader)
+
+        self.assertEqual(spec.name, self.name)
+        self.assertEqual(spec.loader, self.loader)
+        self.assertIs(spec.origin, None)
+        self.assertIs(spec.loader_state, None)
+        self.assertIs(spec.submodule_search_locations, None)
+        self.assertIs(spec.cached, None)
+        self.assertFalse(spec.has_location)
+
+    def test_default_no_loader(self):
+        spec = self.machinery.ModuleSpec(self.name, None)
+
+        self.assertEqual(spec.name, self.name)
+        self.assertIs(spec.loader, None)
+        self.assertIs(spec.origin, None)
+        self.assertIs(spec.loader_state, None)
+        self.assertIs(spec.submodule_search_locations, None)
+        self.assertIs(spec.cached, None)
+        self.assertFalse(spec.has_location)
+
+    def test_default_is_package_false(self):
+        spec = self.machinery.ModuleSpec(self.name, self.loader,
+                                         is_package=False)
+
+        self.assertEqual(spec.name, self.name)
+        self.assertEqual(spec.loader, self.loader)
+        self.assertIs(spec.origin, None)
+        self.assertIs(spec.loader_state, None)
+        self.assertIs(spec.submodule_search_locations, None)
+        self.assertIs(spec.cached, None)
+        self.assertFalse(spec.has_location)
+
+    def test_default_is_package_true(self):
+        spec = self.machinery.ModuleSpec(self.name, self.loader,
+                                         is_package=True)
+
+        self.assertEqual(spec.name, self.name)
+        self.assertEqual(spec.loader, self.loader)
+        self.assertIs(spec.origin, None)
+        self.assertIs(spec.loader_state, None)
+        self.assertEqual(spec.submodule_search_locations, [])
+        self.assertIs(spec.cached, None)
+        self.assertFalse(spec.has_location)
+
+    def test_equality(self):
+        other = type(sys.implementation)(name=self.name,
+                                         loader=self.loader,
+                                         origin=None,
+                                         submodule_search_locations=None,
+                                         has_location=False,
+                                         cached=None,
+                                         )
+
+        self.assertTrue(self.spec == other)
+
+    def test_equality_location(self):
+        other = type(sys.implementation)(name=self.name,
+                                         loader=self.loader,
+                                         origin=self.path,
+                                         submodule_search_locations=None,
+                                         has_location=True,
+                                         cached=self.cached,
+                                         )
+
+        self.assertEqual(self.loc_spec, other)
+
+    def test_inequality(self):
+        other = type(sys.implementation)(name='ham',
+                                         loader=self.loader,
+                                         origin=None,
+                                         submodule_search_locations=None,
+                                         has_location=False,
+                                         cached=None,
+                                         )
+
+        self.assertNotEqual(self.spec, other)
+
+    def test_inequality_incomplete(self):
+        other = type(sys.implementation)(name=self.name,
+                                         loader=self.loader,
+                                         )
+
+        self.assertNotEqual(self.spec, other)
+
+    def test_package(self):
+        spec = self.machinery.ModuleSpec('spam.eggs', self.loader)
+
+        self.assertEqual(spec.parent, 'spam')
+
+    def test_package_is_package(self):
+        spec = self.machinery.ModuleSpec('spam.eggs', self.loader,
+                                         is_package=True)
+
+        self.assertEqual(spec.parent, 'spam.eggs')
+
+    # cached
+
+    def test_cached_set(self):
+        before = self.spec.cached
+        self.spec.cached = 'there'
+        after = self.spec.cached
+
+        self.assertIs(before, None)
+        self.assertEqual(after, 'there')
+
+    def test_cached_no_origin(self):
+        spec = self.machinery.ModuleSpec(self.name, self.loader)
+
+        self.assertIs(spec.cached, None)
+
+    def test_cached_with_origin_not_location(self):
+        spec = self.machinery.ModuleSpec(self.name, self.loader,
+                                         origin=self.path)
+
+        self.assertIs(spec.cached, None)
+
+    def test_cached_source(self):
+        expected = self.util.cache_from_source(self.path)
+
+        self.assertEqual(self.loc_spec.cached, expected)
+
+    def test_cached_source_unknown_suffix(self):
+        self.loc_spec.origin = 'spam.spamspamspam'
+
+        self.assertIs(self.loc_spec.cached, None)
+
+    def test_cached_source_missing_cache_tag(self):
+        original = sys.implementation.cache_tag
+        sys.implementation.cache_tag = None
+        try:
+            cached = self.loc_spec.cached
+        finally:
+            sys.implementation.cache_tag = original
+
+        self.assertIs(cached, None)
+
+    def test_cached_sourceless(self):
+        self.loc_spec.origin = 'spam.pyc'
+
+        self.assertEqual(self.loc_spec.cached, 'spam.pyc')
+
+
+class Frozen_ModuleSpecTests(ModuleSpecTests, unittest.TestCase):
+    util = frozen_util
+    machinery = frozen_machinery
+
+
+class Source_ModuleSpecTests(ModuleSpecTests, unittest.TestCase):
+    util = source_util
+    machinery = source_machinery
+
+
+class ModuleSpecMethodsTests:
+
+    def setUp(self):
+        self.name = 'spam'
+        self.path = 'spam.py'
+        self.cached = self.util.cache_from_source(self.path)
+        self.loader = TestLoader()
+        self.spec = self.machinery.ModuleSpec(self.name, self.loader)
+        self.loc_spec = self.machinery.ModuleSpec(self.name, self.loader,
+                                                  origin=self.path)
+        self.loc_spec._set_fileattr = True
+
+    # init_module_attrs
+
+    def test_init_module_attrs(self):
+        module = type(sys)(self.name)
+        spec = self.machinery.ModuleSpec(self.name, self.loader)
+        self.bootstrap._SpecMethods(spec).init_module_attrs(module)
+
+        self.assertEqual(module.__name__, spec.name)
+        self.assertIs(module.__loader__, spec.loader)
+        self.assertEqual(module.__package__, spec.parent)
+        self.assertIs(module.__spec__, spec)
+        self.assertFalse(hasattr(module, '__path__'))
+        self.assertFalse(hasattr(module, '__file__'))
+        self.assertFalse(hasattr(module, '__cached__'))
+
+    def test_init_module_attrs_package(self):
+        module = type(sys)(self.name)
+        spec = self.machinery.ModuleSpec(self.name, self.loader)
+        spec.submodule_search_locations = ['spam', 'ham']
+        self.bootstrap._SpecMethods(spec).init_module_attrs(module)
+
+        self.assertEqual(module.__name__, spec.name)
+        self.assertIs(module.__loader__, spec.loader)
+        self.assertEqual(module.__package__, spec.parent)
+        self.assertIs(module.__spec__, spec)
+        self.assertIs(module.__path__, spec.submodule_search_locations)
+        self.assertFalse(hasattr(module, '__file__'))
+        self.assertFalse(hasattr(module, '__cached__'))
+
+    def test_init_module_attrs_location(self):
+        module = type(sys)(self.name)
+        spec = self.loc_spec
+        self.bootstrap._SpecMethods(spec).init_module_attrs(module)
+
+        self.assertEqual(module.__name__, spec.name)
+        self.assertIs(module.__loader__, spec.loader)
+        self.assertEqual(module.__package__, spec.parent)
+        self.assertIs(module.__spec__, spec)
+        self.assertFalse(hasattr(module, '__path__'))
+        self.assertEqual(module.__file__, spec.origin)
+        self.assertEqual(module.__cached__,
+                         self.util.cache_from_source(spec.origin))
+
+    def test_init_module_attrs_different_name(self):
+        module = type(sys)('eggs')
+        spec = self.machinery.ModuleSpec(self.name, self.loader)
+        self.bootstrap._SpecMethods(spec).init_module_attrs(module)
+
+        self.assertEqual(module.__name__, spec.name)
+
+    def test_init_module_attrs_different_spec(self):
+        module = type(sys)(self.name)
+        module.__spec__ = self.machinery.ModuleSpec('eggs', object())
+        spec = self.machinery.ModuleSpec(self.name, self.loader)
+        self.bootstrap._SpecMethods(spec).init_module_attrs(module)
+
+        self.assertEqual(module.__name__, spec.name)
+        self.assertIs(module.__loader__, spec.loader)
+        self.assertEqual(module.__package__, spec.parent)
+        self.assertIs(module.__spec__, spec)
+
+    def test_init_module_attrs_already_set(self):
+        module = type(sys)('ham.eggs')
+        module.__loader__ = object()
+        module.__package__ = 'ham'
+        module.__path__ = ['eggs']
+        module.__file__ = 'ham/eggs/__init__.py'
+        module.__cached__ = self.util.cache_from_source(module.__file__)
+        original = vars(module).copy()
+        spec = self.loc_spec
+        spec.submodule_search_locations = ['']
+        self.bootstrap._SpecMethods(spec).init_module_attrs(module)
+
+        self.assertIs(module.__loader__, original['__loader__'])
+        self.assertEqual(module.__package__, original['__package__'])
+        self.assertIs(module.__path__, original['__path__'])
+        self.assertEqual(module.__file__, original['__file__'])
+        self.assertEqual(module.__cached__, original['__cached__'])
+
+    def test_init_module_attrs_immutable(self):
+        module = object()
+        spec = self.loc_spec
+        spec.submodule_search_locations = ['']
+        self.bootstrap._SpecMethods(spec).init_module_attrs(module)
+
+        self.assertFalse(hasattr(module, '__name__'))
+        self.assertFalse(hasattr(module, '__loader__'))
+        self.assertFalse(hasattr(module, '__package__'))
+        self.assertFalse(hasattr(module, '__spec__'))
+        self.assertFalse(hasattr(module, '__path__'))
+        self.assertFalse(hasattr(module, '__file__'))
+        self.assertFalse(hasattr(module, '__cached__'))
+
+    # create()
+
+    def test_create(self):
+        created = self.bootstrap._SpecMethods(self.spec).create()
+
+        self.assertEqual(created.__name__, self.spec.name)
+        self.assertIs(created.__loader__, self.spec.loader)
+        self.assertEqual(created.__package__, self.spec.parent)
+        self.assertIs(created.__spec__, self.spec)
+        self.assertFalse(hasattr(created, '__path__'))
+        self.assertFalse(hasattr(created, '__file__'))
+        self.assertFalse(hasattr(created, '__cached__'))
+
+    def test_create_from_loader(self):
+        module = type(sys.implementation)()
+        class CreatingLoader(TestLoader):
+            def create_module(self, spec):
+                return module
+        self.spec.loader = CreatingLoader()
+        created = self.bootstrap._SpecMethods(self.spec).create()
+
+        self.assertIs(created, module)
+        self.assertEqual(created.__name__, self.spec.name)
+        self.assertIs(created.__loader__, self.spec.loader)
+        self.assertEqual(created.__package__, self.spec.parent)
+        self.assertIs(created.__spec__, self.spec)
+        self.assertFalse(hasattr(created, '__path__'))
+        self.assertFalse(hasattr(created, '__file__'))
+        self.assertFalse(hasattr(created, '__cached__'))
+
+    def test_create_from_loader_not_handled(self):
+        class CreatingLoader(TestLoader):
+            def create_module(self, spec):
+                return None
+        self.spec.loader = CreatingLoader()
+        created = self.bootstrap._SpecMethods(self.spec).create()
+
+        self.assertEqual(created.__name__, self.spec.name)
+        self.assertIs(created.__loader__, self.spec.loader)
+        self.assertEqual(created.__package__, self.spec.parent)
+        self.assertIs(created.__spec__, self.spec)
+        self.assertFalse(hasattr(created, '__path__'))
+        self.assertFalse(hasattr(created, '__file__'))
+        self.assertFalse(hasattr(created, '__cached__'))
+
+    # exec()
+
+    def test_exec(self):
+        self.spec.loader = NewLoader()
+        module = self.bootstrap._SpecMethods(self.spec).create()
+        sys.modules[self.name] = module
+        self.assertFalse(hasattr(module, 'eggs'))
+        self.bootstrap._SpecMethods(self.spec).exec(module)
+
+        self.assertEqual(module.eggs, 1)
+
+    # load()
+
+    def test_load(self):
+        self.spec.loader = NewLoader()
+        with CleanImport(self.spec.name):
+            loaded = self.bootstrap._SpecMethods(self.spec).load()
+            installed = sys.modules[self.spec.name]
+
+        self.assertEqual(loaded.eggs, 1)
+        self.assertIs(loaded, installed)
+
+    def test_load_replaced(self):
+        replacement = object()
+        class ReplacingLoader(TestLoader):
+            def exec_module(self, module):
+                sys.modules[module.__name__] = replacement
+        self.spec.loader = ReplacingLoader()
+        with CleanImport(self.spec.name):
+            loaded = self.bootstrap._SpecMethods(self.spec).load()
+            installed = sys.modules[self.spec.name]
+
+        self.assertIs(loaded, replacement)
+        self.assertIs(installed, replacement)
+
+    def test_load_failed(self):
+        class FailedLoader(TestLoader):
+            def exec_module(self, module):
+                raise RuntimeError
+        self.spec.loader = FailedLoader()
+        with CleanImport(self.spec.name):
+            with self.assertRaises(RuntimeError):
+                loaded = self.bootstrap._SpecMethods(self.spec).load()
+            self.assertNotIn(self.spec.name, sys.modules)
+
+    def test_load_failed_removed(self):
+        class FailedLoader(TestLoader):
+            def exec_module(self, module):
+                del sys.modules[module.__name__]
+                raise RuntimeError
+        self.spec.loader = FailedLoader()
+        with CleanImport(self.spec.name):
+            with self.assertRaises(RuntimeError):
+                loaded = self.bootstrap._SpecMethods(self.spec).load()
+            self.assertNotIn(self.spec.name, sys.modules)
+
+    def test_load_existing(self):
+        existing = type(sys)('ham')
+        existing.count = 5
+        self.spec.loader = NewLoader()
+        with CleanImport(self.name):
+            sys.modules[self.name] = existing
+            assert self.spec.name == self.name
+            loaded = self.bootstrap._SpecMethods(self.spec).load()
+
+        self.assertEqual(loaded.eggs, 1)
+        self.assertFalse(hasattr(loaded, 'ham'))
+
+    def test_load_legacy(self):
+        self.spec.loader = LegacyLoader()
+        with CleanImport(self.spec.name):
+            loaded = self.bootstrap._SpecMethods(self.spec).load()
+
+        self.assertEqual(loaded.ham, -1)
+
+    def test_load_legacy_attributes(self):
+        self.spec.loader = LegacyLoader()
+        with CleanImport(self.spec.name):
+            loaded = self.bootstrap._SpecMethods(self.spec).load()
+
+        self.assertIs(loaded.__loader__, self.spec.loader)
+        self.assertEqual(loaded.__package__, self.spec.parent)
+        self.assertIs(loaded.__spec__, self.spec)
+
+    def test_load_legacy_attributes_immutable(self):
+        module = object()
+        class ImmutableLoader(TestLoader):
+            def load_module(self, name):
+                sys.modules[name] = module
+                return module
+        self.spec.loader = ImmutableLoader()
+        with CleanImport(self.spec.name):
+            loaded = self.bootstrap._SpecMethods(self.spec).load()
+
+            self.assertIs(sys.modules[self.spec.name], module)
+
+    # reload()
+
+    def test_reload(self):
+        self.spec.loader = NewLoader()
+        with CleanImport(self.spec.name):
+            loaded = self.bootstrap._SpecMethods(self.spec).load()
+            reloaded = self.bootstrap._SpecMethods(self.spec).exec(loaded)
+            installed = sys.modules[self.spec.name]
+
+        self.assertEqual(loaded.eggs, 1)
+        self.assertIs(reloaded, loaded)
+        self.assertIs(installed, loaded)
+
+    def test_reload_modified(self):
+        self.spec.loader = NewLoader()
+        with CleanImport(self.spec.name):
+            loaded = self.bootstrap._SpecMethods(self.spec).load()
+            loaded.eggs = 2
+            reloaded = self.bootstrap._SpecMethods(self.spec).exec(loaded)
+
+        self.assertEqual(loaded.eggs, 1)
+        self.assertIs(reloaded, loaded)
+
+    def test_reload_extra_attributes(self):
+        self.spec.loader = NewLoader()
+        with CleanImport(self.spec.name):
+            loaded = self.bootstrap._SpecMethods(self.spec).load()
+            loaded.available = False
+            reloaded = self.bootstrap._SpecMethods(self.spec).exec(loaded)
+
+        self.assertFalse(loaded.available)
+        self.assertIs(reloaded, loaded)
+
+    def test_reload_init_module_attrs(self):
+        self.spec.loader = NewLoader()
+        with CleanImport(self.spec.name):
+            loaded = self.bootstrap._SpecMethods(self.spec).load()
+            loaded.__name__ = 'ham'
+            del loaded.__loader__
+            del loaded.__package__
+            del loaded.__spec__
+            self.bootstrap._SpecMethods(self.spec).exec(loaded)
+
+        self.assertEqual(loaded.__name__, self.spec.name)
+        self.assertIs(loaded.__loader__, self.spec.loader)
+        self.assertEqual(loaded.__package__, self.spec.parent)
+        self.assertIs(loaded.__spec__, self.spec)
+        self.assertFalse(hasattr(loaded, '__path__'))
+        self.assertFalse(hasattr(loaded, '__file__'))
+        self.assertFalse(hasattr(loaded, '__cached__'))
+
+    def test_reload_legacy(self):
+        self.spec.loader = LegacyLoader()
+        with CleanImport(self.spec.name):
+            loaded = self.bootstrap._SpecMethods(self.spec).load()
+            reloaded = self.bootstrap._SpecMethods(self.spec).exec(loaded)
+            installed = sys.modules[self.spec.name]
+
+        self.assertEqual(loaded.ham, -1)
+        self.assertIs(reloaded, loaded)
+        self.assertIs(installed, loaded)
+
+
+class Frozen_ModuleSpecMethodsTests(ModuleSpecMethodsTests, unittest.TestCase):
+    bootstrap = frozen_bootstrap
+    machinery = frozen_machinery
+    util = frozen_util
+
+
+class Source_ModuleSpecMethodsTests(ModuleSpecMethodsTests, unittest.TestCase):
+    bootstrap = source_bootstrap
+    machinery = source_machinery
+    util = source_util
+
+
+class ModuleReprTests:
+
+    # XXX Add more tests for repr(module) once ModuleSpec._module_repr()
+    # is in place?
+
+    def setUp(self):
+        self.module = type(os)('spam')
+        self.spec = self.machinery.ModuleSpec('spam', TestLoader())
+
+    def test_module___loader___module_repr(self):
+        class Loader:
+            def module_repr(self, module):
+                return '<delicious {}>'.format(module.__name__)
+        self.module.__loader__ = Loader()
+        modrepr = self.bootstrap._module_repr(self.module)
+
+        self.assertEqual(modrepr, '<delicious spam>')
+
+    def test_module___loader___module_repr_bad(self):
+        class Loader(TestLoader):
+            def module_repr(self, module):
+                raise Exception
+        self.module.__loader__ = Loader()
+        modrepr = self.bootstrap._module_repr(self.module)
+
+        self.assertEqual(modrepr,
+                         '<module {!r} (<TestLoader object>)>'.format('spam'))
+
+    def test_module___spec__(self):
+        origin = 'in a hole, in the ground'
+        self.spec.origin = origin
+        self.module.__spec__ = self.spec
+        modrepr = self.bootstrap._module_repr(self.module)
+
+        self.assertEqual(modrepr, '<module {!r} ({})>'.format('spam', origin))
+
+    def test_module___spec___location(self):
+        location = 'in_a_galaxy_far_far_away.py'
+        self.spec.origin = location
+        self.spec._set_fileattr = True
+        self.module.__spec__ = self.spec
+        modrepr = self.bootstrap._module_repr(self.module)
+
+        self.assertEqual(modrepr,
+                         '<module {!r} from {!r}>'.format('spam', location))
+
+    def test_module___spec___no_origin(self):
+        self.spec.loader = TestLoader()
+        self.module.__spec__ = self.spec
+        modrepr = self.bootstrap._module_repr(self.module)
+
+        self.assertEqual(modrepr,
+                         '<module {!r} (<TestLoader object>)>'.format('spam'))
+
+    def test_module___spec___no_origin_no_loader(self):
+        self.spec.loader = None
+        self.module.__spec__ = self.spec
+        modrepr = self.bootstrap._module_repr(self.module)
+
+        self.assertEqual(modrepr, '<module {!r}>'.format('spam'))
+
+    def test_module_no_name(self):
+        del self.module.__name__
+        modrepr = self.bootstrap._module_repr(self.module)
+
+        self.assertEqual(modrepr, '<module {!r}>'.format('?'))
+
+    def test_module_with_file(self):
+        filename = 'e/i/e/i/o/spam.py'
+        self.module.__file__ = filename
+        modrepr = self.bootstrap._module_repr(self.module)
+
+        self.assertEqual(modrepr,
+                         '<module {!r} from {!r}>'.format('spam', filename))
+
+    def test_module_no_file(self):
+        self.module.__loader__ = TestLoader()
+        modrepr = self.bootstrap._module_repr(self.module)
+
+        self.assertEqual(modrepr,
+                         '<module {!r} (<TestLoader object>)>'.format('spam'))
+
+    def test_module_no_file_no_loader(self):
+        modrepr = self.bootstrap._module_repr(self.module)
+
+        self.assertEqual(modrepr, '<module {!r}>'.format('spam'))
+
+
+class Frozen_ModuleReprTests(ModuleReprTests, unittest.TestCase):
+    bootstrap = frozen_bootstrap
+    machinery = frozen_machinery
+    util = frozen_util
+
+
+class Source_ModuleReprTests(ModuleReprTests, unittest.TestCase):
+    bootstrap = source_bootstrap
+    machinery = source_machinery
+    util = source_util
+
+
+class FactoryTests:
+
+    def setUp(self):
+        self.name = 'spam'
+        self.path = 'spam.py'
+        self.cached = self.util.cache_from_source(self.path)
+        self.loader = TestLoader()
+        self.fileloader = TestLoader(self.path)
+        self.pkgloader = TestLoader(self.path, True)
+
+    # spec_from_loader()
+
+    def test_spec_from_loader_default(self):
+        spec = self.util.spec_from_loader(self.name, self.loader)
+
+        self.assertEqual(spec.name, self.name)
+        self.assertEqual(spec.loader, self.loader)
+        self.assertIs(spec.origin, None)
+        self.assertIs(spec.loader_state, None)
+        self.assertIs(spec.submodule_search_locations, None)
+        self.assertIs(spec.cached, None)
+        self.assertFalse(spec.has_location)
+
+    def test_spec_from_loader_default_with_bad_is_package(self):
+        class Loader:
+            def is_package(self, name):
+                raise ImportError
+        loader = Loader()
+        spec = self.util.spec_from_loader(self.name, loader)
+
+        self.assertEqual(spec.name, self.name)
+        self.assertEqual(spec.loader, loader)
+        self.assertIs(spec.origin, None)
+        self.assertIs(spec.loader_state, None)
+        self.assertIs(spec.submodule_search_locations, None)
+        self.assertIs(spec.cached, None)
+        self.assertFalse(spec.has_location)
+
+    def test_spec_from_loader_origin(self):
+        origin = 'somewhere over the rainbow'
+        spec = self.util.spec_from_loader(self.name, self.loader,
+                                          origin=origin)
+
+        self.assertEqual(spec.name, self.name)
+        self.assertEqual(spec.loader, self.loader)
+        self.assertIs(spec.origin, origin)
+        self.assertIs(spec.loader_state, None)
+        self.assertIs(spec.submodule_search_locations, None)
+        self.assertIs(spec.cached, None)
+        self.assertFalse(spec.has_location)
+
+    def test_spec_from_loader_is_package_false(self):
+        spec = self.util.spec_from_loader(self.name, self.loader,
+                                          is_package=False)
+
+        self.assertEqual(spec.name, self.name)
+        self.assertEqual(spec.loader, self.loader)
+        self.assertIs(spec.origin, None)
+        self.assertIs(spec.loader_state, None)
+        self.assertIs(spec.submodule_search_locations, None)
+        self.assertIs(spec.cached, None)
+        self.assertFalse(spec.has_location)
+
+    def test_spec_from_loader_is_package_true(self):
+        spec = self.util.spec_from_loader(self.name, self.loader,
+                                          is_package=True)
+
+        self.assertEqual(spec.name, self.name)
+        self.assertEqual(spec.loader, self.loader)
+        self.assertIs(spec.origin, None)
+        self.assertIs(spec.loader_state, None)
+        self.assertEqual(spec.submodule_search_locations, [])
+        self.assertIs(spec.cached, None)
+        self.assertFalse(spec.has_location)
+
+    def test_spec_from_loader_origin_and_is_package(self):
+        origin = 'where the streets have no name'
+        spec = self.util.spec_from_loader(self.name, self.loader,
+                                          origin=origin, is_package=True)
+
+        self.assertEqual(spec.name, self.name)
+        self.assertEqual(spec.loader, self.loader)
+        self.assertIs(spec.origin, origin)
+        self.assertIs(spec.loader_state, None)
+        self.assertEqual(spec.submodule_search_locations, [])
+        self.assertIs(spec.cached, None)
+        self.assertFalse(spec.has_location)
+
+    def test_spec_from_loader_is_package_with_loader_false(self):
+        loader = TestLoader(is_package=False)
+        spec = self.util.spec_from_loader(self.name, loader)
+
+        self.assertEqual(spec.name, self.name)
+        self.assertEqual(spec.loader, loader)
+        self.assertIs(spec.origin, None)
+        self.assertIs(spec.loader_state, None)
+        self.assertIs(spec.submodule_search_locations, None)
+        self.assertIs(spec.cached, None)
+        self.assertFalse(spec.has_location)
+
+    def test_spec_from_loader_is_package_with_loader_true(self):
+        loader = TestLoader(is_package=True)
+        spec = self.util.spec_from_loader(self.name, loader)
+
+        self.assertEqual(spec.name, self.name)
+        self.assertEqual(spec.loader, loader)
+        self.assertIs(spec.origin, None)
+        self.assertIs(spec.loader_state, None)
+        self.assertEqual(spec.submodule_search_locations, [])
+        self.assertIs(spec.cached, None)
+        self.assertFalse(spec.has_location)
+
+    def test_spec_from_loader_default_with_file_loader(self):
+        spec = self.util.spec_from_loader(self.name, self.fileloader)
+
+        self.assertEqual(spec.name, self.name)
+        self.assertEqual(spec.loader, self.fileloader)
+        self.assertEqual(spec.origin, self.path)
+        self.assertIs(spec.loader_state, None)
+        self.assertIs(spec.submodule_search_locations, None)
+        self.assertEqual(spec.cached, self.cached)
+        self.assertTrue(spec.has_location)
+
+    def test_spec_from_loader_is_package_false_with_fileloader(self):
+        spec = self.util.spec_from_loader(self.name, self.fileloader,
+                                          is_package=False)
+
+        self.assertEqual(spec.name, self.name)
+        self.assertEqual(spec.loader, self.fileloader)
+        self.assertEqual(spec.origin, self.path)
+        self.assertIs(spec.loader_state, None)
+        self.assertIs(spec.submodule_search_locations, None)
+        self.assertEqual(spec.cached, self.cached)
+        self.assertTrue(spec.has_location)
+
+    def test_spec_from_loader_is_package_true_with_fileloader(self):
+        spec = self.util.spec_from_loader(self.name, self.fileloader,
+                                          is_package=True)
+
+        self.assertEqual(spec.name, self.name)
+        self.assertEqual(spec.loader, self.fileloader)
+        self.assertEqual(spec.origin, self.path)
+        self.assertIs(spec.loader_state, None)
+        self.assertEqual(spec.submodule_search_locations, [''])
+        self.assertEqual(spec.cached, self.cached)
+        self.assertTrue(spec.has_location)
+
+    # spec_from_file_location()
+
+    def test_spec_from_file_location_default(self):
+        if self.machinery is source_machinery:
+            raise unittest.SkipTest('not sure why this is breaking...')
+        spec = self.util.spec_from_file_location(self.name, self.path)
+
+        self.assertEqual(spec.name, self.name)
+        self.assertIsInstance(spec.loader,
+                              self.machinery.SourceFileLoader)
+        self.assertEqual(spec.loader.name, self.name)
+        self.assertEqual(spec.loader.path, self.path)
+        self.assertEqual(spec.origin, self.path)
+        self.assertIs(spec.loader_state, None)
+        self.assertIs(spec.submodule_search_locations, None)
+        self.assertEqual(spec.cached, self.cached)
+        self.assertTrue(spec.has_location)
+
+    def test_spec_from_file_location_default_without_location(self):
+        spec = self.util.spec_from_file_location(self.name)
+
+        self.assertIs(spec, None)
+
+    def test_spec_from_file_location_default_bad_suffix(self):
+        spec = self.util.spec_from_file_location(self.name, 'spam.eggs')
+
+        self.assertIs(spec, None)
+
+    def test_spec_from_file_location_loader_no_location(self):
+        spec = self.util.spec_from_file_location(self.name,
+                                                 loader=self.fileloader)
+
+        self.assertEqual(spec.name, self.name)
+        self.assertEqual(spec.loader, self.fileloader)
+        self.assertEqual(spec.origin, self.path)
+        self.assertIs(spec.loader_state, None)
+        self.assertIs(spec.submodule_search_locations, None)
+        self.assertEqual(spec.cached, self.cached)
+        self.assertTrue(spec.has_location)
+
+    def test_spec_from_file_location_loader_no_location_no_get_filename(self):
+        spec = self.util.spec_from_file_location(self.name,
+                                                 loader=self.loader)
+
+        self.assertEqual(spec.name, self.name)
+        self.assertEqual(spec.loader, self.loader)
+        self.assertEqual(spec.origin, '<unknown>')
+        self.assertIs(spec.loader_state, None)
+        self.assertIs(spec.submodule_search_locations, None)
+        self.assertIs(spec.cached, None)
+        self.assertTrue(spec.has_location)
+
+    def test_spec_from_file_location_loader_no_location_bad_get_filename(self):
+        class Loader:
+            def get_filename(self, name):
+                raise ImportError
+        loader = Loader()
+        spec = self.util.spec_from_file_location(self.name, loader=loader)
+
+        self.assertEqual(spec.name, self.name)
+        self.assertEqual(spec.loader, loader)
+        self.assertEqual(spec.origin, '<unknown>')
+        self.assertIs(spec.loader_state, None)
+        self.assertIs(spec.submodule_search_locations, None)
+        self.assertIs(spec.cached, None)
+        self.assertTrue(spec.has_location)
+
+    def test_spec_from_file_location_smsl_none(self):
+        spec = self.util.spec_from_file_location(self.name, self.path,
+                                       loader=self.fileloader,
+                                       submodule_search_locations=None)
+
+        self.assertEqual(spec.name, self.name)
+        self.assertEqual(spec.loader, self.fileloader)
+        self.assertEqual(spec.origin, self.path)
+        self.assertIs(spec.loader_state, None)
+        self.assertIs(spec.submodule_search_locations, None)
+        self.assertEqual(spec.cached, self.cached)
+        self.assertTrue(spec.has_location)
+
+    def test_spec_from_file_location_smsl_empty(self):
+        spec = self.util.spec_from_file_location(self.name, self.path,
+                                       loader=self.fileloader,
+                                       submodule_search_locations=[])
+
+        self.assertEqual(spec.name, self.name)
+        self.assertEqual(spec.loader, self.fileloader)
+        self.assertEqual(spec.origin, self.path)
+        self.assertIs(spec.loader_state, None)
+        self.assertEqual(spec.submodule_search_locations, [''])
+        self.assertEqual(spec.cached, self.cached)
+        self.assertTrue(spec.has_location)
+
+    def test_spec_from_file_location_smsl_not_empty(self):
+        spec = self.util.spec_from_file_location(self.name, self.path,
+                                       loader=self.fileloader,
+                                       submodule_search_locations=['eggs'])
+
+        self.assertEqual(spec.name, self.name)
+        self.assertEqual(spec.loader, self.fileloader)
+        self.assertEqual(spec.origin, self.path)
+        self.assertIs(spec.loader_state, None)
+        self.assertEqual(spec.submodule_search_locations, ['eggs'])
+        self.assertEqual(spec.cached, self.cached)
+        self.assertTrue(spec.has_location)
+
+    def test_spec_from_file_location_smsl_default(self):
+        spec = self.util.spec_from_file_location(self.name, self.path,
+                                       loader=self.pkgloader)
+
+        self.assertEqual(spec.name, self.name)
+        self.assertEqual(spec.loader, self.pkgloader)
+        self.assertEqual(spec.origin, self.path)
+        self.assertIs(spec.loader_state, None)
+        self.assertEqual(spec.submodule_search_locations, [''])
+        self.assertEqual(spec.cached, self.cached)
+        self.assertTrue(spec.has_location)
+
+    def test_spec_from_file_location_smsl_default_not_package(self):
+        class Loader:
+            def is_package(self, name):
+                return False
+        loader = Loader()
+        spec = self.util.spec_from_file_location(self.name, self.path,
+                                                 loader=loader)
+
+        self.assertEqual(spec.name, self.name)
+        self.assertEqual(spec.loader, loader)
+        self.assertEqual(spec.origin, self.path)
+        self.assertIs(spec.loader_state, None)
+        self.assertIs(spec.submodule_search_locations, None)
+        self.assertEqual(spec.cached, self.cached)
+        self.assertTrue(spec.has_location)
+
+    def test_spec_from_file_location_smsl_default_no_is_package(self):
+        spec = self.util.spec_from_file_location(self.name, self.path,
+                                       loader=self.fileloader)
+
+        self.assertEqual(spec.name, self.name)
+        self.assertEqual(spec.loader, self.fileloader)
+        self.assertEqual(spec.origin, self.path)
+        self.assertIs(spec.loader_state, None)
+        self.assertIs(spec.submodule_search_locations, None)
+        self.assertEqual(spec.cached, self.cached)
+        self.assertTrue(spec.has_location)
+
+    def test_spec_from_file_location_smsl_default_bad_is_package(self):
+        class Loader:
+            def is_package(self, name):
+                raise ImportError
+        loader = Loader()
+        spec = self.util.spec_from_file_location(self.name, self.path,
+                                                 loader=loader)
+
+        self.assertEqual(spec.name, self.name)
+        self.assertEqual(spec.loader, loader)
+        self.assertEqual(spec.origin, self.path)
+        self.assertIs(spec.loader_state, None)
+        self.assertIs(spec.submodule_search_locations, None)
+        self.assertEqual(spec.cached, self.cached)
+        self.assertTrue(spec.has_location)
+
+
+class Frozen_FactoryTests(FactoryTests, unittest.TestCase):
+    util = frozen_util
+    machinery = frozen_machinery
+
+
+class Source_FactoryTests(FactoryTests, unittest.TestCase):
+    util = source_util
+    machinery = source_machinery
diff --git a/Lib/test/test_importlib/test_util.py b/Lib/test/test_importlib/test_util.py
--- a/Lib/test/test_importlib/test_util.py
+++ b/Lib/test/test_importlib/test_util.py
@@ -34,71 +34,6 @@
         DecodeSourceBytesTests, util=[frozen_util, source_util])
 
 
-class ModuleToLoadTests:
-
-    module_name = 'ModuleManagerTest_module'
-
-    def setUp(self):
-        support.unload(self.module_name)
-        self.addCleanup(support.unload, self.module_name)
-
-    def test_new_module(self):
-        # Test a new module is created, inserted into sys.modules, has
-        # __initializing__ set to True after entering the context manager,
-        # and __initializing__ set to False after exiting.
-        with self.util.module_to_load(self.module_name) as module:
-            self.assertIn(self.module_name, sys.modules)
-            self.assertIs(sys.modules[self.module_name], module)
-            self.assertTrue(module.__initializing__)
-        self.assertFalse(module.__initializing__)
-
-    def test_new_module_failed(self):
-        # Test the module is removed from sys.modules.
-        try:
-            with self.util.module_to_load(self.module_name) as module:
-                self.assertIn(self.module_name, sys.modules)
-                raise exception
-        except Exception:
-            self.assertNotIn(self.module_name, sys.modules)
-        else:
-            self.fail('importlib.util.module_to_load swallowed an exception')
-
-    def test_reload(self):
-        # Test that the same module is in sys.modules.
-        created_module = types.ModuleType(self.module_name)
-        sys.modules[self.module_name] = created_module
-        with self.util.module_to_load(self.module_name) as module:
-            self.assertIs(module, created_module)
-
-    def test_reload_failed(self):
-        # Test that the module was left in sys.modules.
-        created_module = types.ModuleType(self.module_name)
-        sys.modules[self.module_name] = created_module
-        try:
-            with self.util.module_to_load(self.module_name) as module:
-                raise Exception
-        except Exception:
-            self.assertIn(self.module_name, sys.modules)
-        else:
-            self.fail('importlib.util.module_to_load swallowed an exception')
-
-    def test_reset_name(self):
-        # If reset_name is true then module.__name__ = name, else leave it be.
-        odd_name = 'not your typical name'
-        created_module = types.ModuleType(self.module_name)
-        created_module.__name__ = odd_name
-        sys.modules[self.module_name] = created_module
-        with self.util.module_to_load(self.module_name) as module:
-            self.assertEqual(module.__name__, self.module_name)
-        created_module.__name__ = odd_name
-        with self.util.module_to_load(self.module_name, reset_name=False) as module:
-            self.assertEqual(module.__name__, odd_name)
-
-Frozen_ModuleToLoadTests, Source_ModuleToLoadTests = test_util.test_both(
-        ModuleToLoadTests,
-        util=[frozen_util, source_util])
-
-
 class ModuleForLoaderTests:
 
     """Tests for importlib.util.module_for_loader."""
diff --git a/Lib/test/test_module.py b/Lib/test/test_module.py
--- a/Lib/test/test_module.py
+++ b/Lib/test/test_module.py
@@ -37,8 +37,10 @@
         self.assertEqual(foo.__doc__, None)
         self.assertIs(foo.__loader__, None)
         self.assertIs(foo.__package__, None)
+        self.assertIs(foo.__spec__, None)
         self.assertEqual(foo.__dict__, {"__name__": "foo", "__doc__": None,
-                                        "__loader__": None, "__package__": None})
+                                        "__loader__": None, "__package__": None,
+                                        "__spec__": None})
 
     def test_ascii_docstring(self):
         # ASCII docstring
@@ -47,7 +49,8 @@
         self.assertEqual(foo.__doc__, "foodoc")
         self.assertEqual(foo.__dict__,
                          {"__name__": "foo", "__doc__": "foodoc",
-                          "__loader__": None, "__package__": None})
+                          "__loader__": None, "__package__": None,
+                          "__spec__": None})
 
     def test_unicode_docstring(self):
         # Unicode docstring
@@ -56,7 +59,8 @@
         self.assertEqual(foo.__doc__, "foodoc\u1234")
         self.assertEqual(foo.__dict__,
                          {"__name__": "foo", "__doc__": "foodoc\u1234",
-                          "__loader__": None, "__package__": None})
+                          "__loader__": None, "__package__": None,
+                          "__spec__": None})
 
     def test_reinit(self):
         # Reinitialization should not replace the __dict__
@@ -69,7 +73,7 @@
         self.assertEqual(foo.bar, 42)
         self.assertEqual(foo.__dict__,
               {"__name__": "foo", "__doc__": "foodoc", "bar": 42,
-               "__loader__": None, "__package__": None})
+               "__loader__": None, "__package__": None, "__spec__": None})
         self.assertTrue(foo.__dict__ is d)
 
     def test_dont_clear_dict(self):
diff --git a/Lib/test/test_namespace_pkgs.py b/Lib/test/test_namespace_pkgs.py
--- a/Lib/test/test_namespace_pkgs.py
+++ b/Lib/test/test_namespace_pkgs.py
@@ -1,5 +1,4 @@
 import contextlib
-from importlib._bootstrap import NamespaceLoader
 import importlib.abc
 import importlib.machinery
 import os
@@ -290,24 +289,5 @@
         self.assertEqual(a_test.attr, 'in module')
 
 
-class ABCTests(unittest.TestCase):
-
-    def setUp(self):
-        self.loader = NamespaceLoader('foo', ['pkg'],
-                                      importlib.machinery.PathFinder)
-
-    def test_is_package(self):
-        self.assertTrue(self.loader.is_package('foo'))
-
-    def test_get_code(self):
-        self.assertTrue(isinstance(self.loader.get_code('foo'), types.CodeType))
-
-    def test_get_source(self):
-        self.assertEqual(self.loader.get_source('foo'), '')
-
-    def test_abc_isinstance(self):
-        self.assertTrue(isinstance(self.loader, importlib.abc.InspectLoader))
-
-
 if __name__ == "__main__":
     unittest.main()
diff --git a/Lib/test/test_pkg.py b/Lib/test/test_pkg.py
--- a/Lib/test/test_pkg.py
+++ b/Lib/test/test_pkg.py
@@ -199,14 +199,14 @@
         import t5
         self.assertEqual(fixdir(dir(t5)),
                          ['__cached__', '__doc__', '__file__', '__loader__',
-                          '__name__', '__package__', '__path__', 'foo',
-                          'string', 't5'])
+                          '__name__', '__package__', '__path__', '__spec__',
+                          'foo', 'string', 't5'])
         self.assertEqual(fixdir(dir(t5.foo)),
                          ['__cached__', '__doc__', '__file__', '__loader__',
-                          '__name__', '__package__', 'string'])
+                          '__name__', '__package__', '__spec__', 'string'])
         self.assertEqual(fixdir(dir(t5.string)),
                          ['__cached__', '__doc__', '__file__', '__loader__',
-                          '__name__', '__package__', 'spam'])
+                          '__name__', '__package__', '__spec__', 'spam'])
 
     def test_6(self):
         hier = [
@@ -222,14 +222,15 @@
         import t6
         self.assertEqual(fixdir(dir(t6)),
                          ['__all__', '__cached__', '__doc__', '__file__',
-                          '__loader__', '__name__', '__package__', '__path__'])
+                          '__loader__', '__name__', '__package__', '__path__',
+                          '__spec__'])
         s = """
             import t6
             from t6 import *
             self.assertEqual(fixdir(dir(t6)),
                              ['__all__', '__cached__', '__doc__', '__file__',
                               '__loader__', '__name__', '__package__',
-                              '__path__', 'eggs', 'ham', 'spam'])
+                              '__path__', '__spec__', 'eggs', 'ham', 'spam'])
             self.assertEqual(dir(), ['eggs', 'ham', 'self', 'spam', 't6'])
             """
         self.run_code(s)
@@ -256,18 +257,19 @@
         import t7 as tas
         self.assertEqual(fixdir(dir(tas)),
                          ['__cached__', '__doc__', '__file__', '__loader__',
-                          '__name__', '__package__', '__path__'])
+                          '__name__', '__package__', '__path__', '__spec__'])
         self.assertFalse(t7)
         from t7 import sub as subpar
         self.assertEqual(fixdir(dir(subpar)),
                          ['__cached__', '__doc__', '__file__', '__loader__',
-                          '__name__', '__package__', '__path__'])
+                          '__name__', '__package__', '__path__', '__spec__'])
         self.assertFalse(t7)
         self.assertFalse(sub)
         from t7.sub import subsub as subsubsub
         self.assertEqual(fixdir(dir(subsubsub)),
                          ['__cached__', '__doc__', '__file__', '__loader__',
-                          '__name__', '__package__', '__path__', 'spam'])
+                          '__name__', '__package__', '__path__', '__spec__',
+                          'spam'])
         self.assertFalse(t7)
         self.assertFalse(sub)
         self.assertFalse(subsub)
diff --git a/Lib/test/test_pkgutil.py b/Lib/test/test_pkgutil.py
--- a/Lib/test/test_pkgutil.py
+++ b/Lib/test/test_pkgutil.py
@@ -208,9 +208,16 @@
             importers = list(iter_importers(fullname))
             expected_importer = get_importer(pathitem)
             for finder in importers:
+                loader = finder.find_module(fullname)
+                try:
+                    loader = loader.loader
+                except AttributeError:
+                    # For now we still allow raw loaders from
+                    # find_module().
+                    pass
                 self.assertIsInstance(finder, importlib.machinery.FileFinder)
                 self.assertEqual(finder, expected_importer)
-                self.assertIsInstance(finder.find_module(fullname),
+                self.assertIsInstance(loader,
                                       importlib.machinery.SourceFileLoader)
                 self.assertIsNone(finder.find_module(pkgname))
 
@@ -222,8 +229,11 @@
         finally:
             shutil.rmtree(dirname)
             del sys.path[0]
-            del sys.modules['spam']
-            del sys.modules['spam.eggs']
+            try:
+                del sys.modules['spam']
+                del sys.modules['spam.eggs']
+            except KeyError:
+                pass
 
 
     def test_mixed_namespace(self):
diff --git a/Lib/test/test_reprlib.py b/Lib/test/test_reprlib.py
--- a/Lib/test/test_reprlib.py
+++ b/Lib/test/test_reprlib.py
@@ -253,6 +253,7 @@
             print("cached_path_len =", cached_path_len)
 
     def test_module(self):
+        self.maxDiff = None
         self._check_path_limitations(self.pkgname)
         create_empty_file(os.path.join(self.subpkgname, self.pkgname + '.py'))
         importlib.invalidate_caches()
diff --git a/Lib/test/test_runpy.py b/Lib/test/test_runpy.py
--- a/Lib/test/test_runpy.py
+++ b/Lib/test/test_runpy.py
@@ -47,6 +47,7 @@
     "__cached__": None,
     "__package__": None,
     "__doc__": None,
+#    "__spec__": None,  # XXX Uncomment.
 }
 example_namespace =  {
     "sys": sys,
@@ -56,7 +57,7 @@
     "run_name_in_sys_modules": False,
     "module_in_sys_modules": False,
     "nested": dict(implicit_namespace,
-                   x=1, __name__="<run>", __loader__=None),
+                   x=1, __name__="<run>", __loader__=None, __spec__=None),
 }
 example_namespace.update(implicit_namespace)
 
@@ -243,6 +244,7 @@
             "__name__": mod_name,
             "__file__": mod_fname,
             "__package__": mod_name.rpartition(".")[0],
+#            "__spec__": None,  # XXX Needs to be set.
         })
         if alter_sys:
             expected_ns.update({
@@ -279,6 +281,7 @@
             "__name__": mod_name,
             "__file__": mod_fname,
             "__package__": pkg_name,
+#            "__spec__": None,  # XXX Needs to be set.
         })
         if alter_sys:
             expected_ns.update({
diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c
--- a/Objects/moduleobject.c
+++ b/Objects/moduleobject.c
@@ -45,6 +45,8 @@
         return -1;
     if (PyDict_SetItemString(md_dict, "__loader__", Py_None) != 0)
         return -1;
+    if (PyDict_SetItemString(md_dict, "__spec__", Py_None) != 0)
+        return -1;
     if (PyUnicode_CheckExact(name)) {
         Py_INCREF(name);
         Py_XDECREF(mod->md_name);
@@ -398,55 +400,10 @@
 static PyObject *
 module_repr(PyModuleObject *m)
 {
-    PyObject *name, *filename, *repr, *loader = NULL;
+    PyThreadState *tstate = PyThreadState_GET();
+    PyInterpreterState *interp = tstate->interp;
 
-    /* See if the module has an __loader__.  If it does, give the loader the
-     * first shot at producing a repr for the module.
-     */
-    if (m->md_dict != NULL) {
-        loader = PyDict_GetItemString(m->md_dict, "__loader__");
-    }
-    if (loader != NULL && loader != Py_None) {
-        repr = PyObject_CallMethod(loader, "module_repr", "(O)",
-                                   (PyObject *)m, NULL);
-        if (repr == NULL) {
-            PyErr_Clear();
-        }
-        else {
-            return repr;
-        }
-    }
-    /* __loader__.module_repr(m) did not provide us with a repr.  Next, see if
-     * the module has an __file__.  If it doesn't then use repr(__loader__) if
-     * it exists, otherwise, just use module.__name__.
-     */
-    name = PyModule_GetNameObject((PyObject *)m);
-    if (name == NULL) {
-        PyErr_Clear();
-        name = PyUnicode_FromStringAndSize("?", 1);
-        if (name == NULL)
-            return NULL;
-    }
-    filename = PyModule_GetFilenameObject((PyObject *)m);
-    if (filename == NULL) {
-        PyErr_Clear();
-        /* There's no m.__file__, so if there was a __loader__, use that in
-         * the repr, otherwise, the only thing you can use is m.__name__
-         */
-        if (loader == NULL || loader == Py_None) {
-            repr = PyUnicode_FromFormat("<module %R>", name);
-        }
-        else {
-            repr = PyUnicode_FromFormat("<module %R (%R)>", name, loader);
-        }
-    }
-    /* Finally, use m.__file__ */
-    else {
-        repr = PyUnicode_FromFormat("<module %R from %R>", name, filename);
-        Py_DECREF(filename);
-    }
-    Py_DECREF(name);
-    return repr;
+    return PyObject_CallMethod(interp->importlib, "_module_repr", "O", m);
 }
 
 static int
diff --git a/Python/import.c b/Python/import.c
--- a/Python/import.c
+++ b/Python/import.c
@@ -1232,7 +1232,8 @@
                                  int level)
 {
     _Py_IDENTIFIER(__import__);
-    _Py_IDENTIFIER(__initializing__);
+    _Py_IDENTIFIER(__spec__);
+    _Py_IDENTIFIER(_initializing);
     _Py_IDENTIFIER(__package__);
     _Py_IDENTIFIER(__path__);
     _Py_IDENTIFIER(__name__);
@@ -1426,16 +1427,21 @@
         goto error_with_unlock;
     }
     else if (mod != NULL) {
-        PyObject *value;
+        PyObject *value = NULL;
+        PyObject *spec;
         int initializing = 0;
 
         Py_INCREF(mod);
         /* Optimization: only call _bootstrap._lock_unlock_module() if
-           __initializing__ is true.
-           NOTE: because of this, __initializing__ must be set *before*
+           __spec__._initializing is true.
+           NOTE: because of this, initializing must be set *before*
            stuffing the new module in sys.modules.
          */
-        value = _PyObject_GetAttrId(mod, &PyId___initializing__);
+        spec = _PyObject_GetAttrId(mod, &PyId___spec__);
+        if (spec != NULL) {
+            value = _PyObject_GetAttrId(spec, &PyId__initializing);
+            Py_DECREF(spec);
+        }
         if (value == NULL)
             PyErr_Clear();
         else {
diff --git a/Python/importlib.h b/Python/importlib.h
--- a/Python/importlib.h
+++ b/Python/importlib.h
[stripped]

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


More information about the Python-checkins mailing list