Implement the PEP 302 protocol for get_filename() as
importlib.abc.ExecutionLoader. PyLoader now inherits from this ABC instead of
InspectLoader directly. Both PyLoader and PyPycLoader provide concrete
implementations of get_filename in terms of source_path and bytecode_path.
diff --git a/Doc/library/importlib.rst b/Doc/library/importlib.rst
index 7ae696d..e051472 100644
--- a/Doc/library/importlib.rst
+++ b/Doc/library/importlib.rst
@@ -202,10 +202,24 @@
:term:`loader` cannot find the module.
+.. class:: ExecutionLoader
+
+ An abstract base class which inherits from :class:`InspectLoader` that,
+ when implemented, allows a module to be executed as a script. The ABC
+ represents an optional :pep:`302` protocol.
+
+ .. method:: get_filename(fullname)
+
+ An abstract method that is to return the value for :attr:`__file__` for
+ the specified module. If no path is available, :exc:`ImportError` is
+ raised.
+
+
.. class:: PyLoader
- An abstract base class inheriting from :class:`importlib.abc.InspectLoader`
- and :class:`importlib.abc.ResourceLoader` designed to ease the loading of
+ An abstract base class inheriting from
+ :class:`importlib.abc.ExecutionLoader` and
+ :class:`importlib.abc.ResourceLoader` designed to ease the loading of
Python source modules (bytecode is not handled; see
:class:`importlib.abc.PyPycLoader` for a source/bytecode ABC). A subclass
implementing this ABC will only need to worry about exposing how the source
@@ -218,6 +232,13 @@
module. Should return :keyword:`None` if there is no source code.
:exc:`ImportError` if the module cannot be found.
+ .. method:: get_filename(fullname)
+
+ A concrete implementation of
+ :meth:`importlib.abc.ExecutionLoader.get_filename` that
+ relies on :meth:`source_path`. If :meth:`source_path` returns
+ :keyword:`None`, then :exc:`ImportError` is raised.
+
.. method:: load_module(fullname)
A concrete implementation of :meth:`importlib.abc.Loader.load_module`
@@ -238,8 +259,8 @@
A concrete implementation of
:meth:`importlib.abc.InspectLoader.get_source`. Uses
- :meth:`importlib.abc.InspectLoader.get_data` and :meth:`source_path` to
- get the source code. It tries to guess the source encoding using
+ :meth:`importlib.abc.ResourceLoader.get_data` and :meth:`source_path`
+ to get the source code. It tries to guess the source encoding using
:func:`tokenize.detect_encoding`.
@@ -253,7 +274,7 @@
An abstract method which returns the modification time for the source
code of the specified module. The modification time should be an
- integer. If there is no source code, return :keyword:`None. If the
+ integer. If there is no source code, return :keyword:`None`. If the
module cannot be found then :exc:`ImportError` is raised.
.. method:: bytecode_path(fullname)
@@ -263,6 +284,16 @@
if no bytecode exists (yet).
Raises :exc:`ImportError` if the module is not found.
+ .. method:: get_filename(fullname)
+
+ A concrete implementation of
+ :meth:`importlib.abc.ExecutionLoader.get_filename` that relies on
+ :meth:`importlib.abc.PyLoader.source_path` and :meth:`bytecode_path`.
+ If :meth:`source_path` returns a path, then that value is returned.
+ Else if :meth:`bytecode_path` returns a path, that path will be
+ returned. If a path is not available from both methods,
+ :exc:`ImportError` is raised.
+
.. method:: write_bytecode(fullname, bytecode)
An abstract method which has the loader write *bytecode* for future
diff --git a/Lib/importlib/_bootstrap.py b/Lib/importlib/_bootstrap.py
index ee3f1e6..2c5a1cf 100644
--- a/Lib/importlib/_bootstrap.py
+++ b/Lib/importlib/_bootstrap.py
@@ -315,16 +315,10 @@
@module_for_loader
def load_module(self, module):
- """Load a source module."""
- return self._load_module(module)
-
- def _load_module(self, module):
- """Initialize a module from source."""
+ """Initialize the module."""
name = module.__name__
code_object = self.get_code(module.__name__)
- # __file__ may have been set by the caller, e.g. bytecode path.
- if not hasattr(module, '__file__'):
- module.__file__ = self.source_path(name)
+ module.__file__ = self.get_filename(name)
if self.is_package(name):
module.__path__ = [module.__file__.rsplit(path_sep, 1)[0]]
module.__package__ = module.__name__
@@ -334,6 +328,15 @@
exec(code_object, module.__dict__)
return module
+ def get_filename(self, fullname):
+ """Return the path to the source file, else raise ImportError."""
+ path = self.source_path(fullname)
+ if path is not None:
+ return path
+ else:
+ raise ImportError("no source path available for "
+ "{0!r}".format(fullname))
+
def get_code(self, fullname):
"""Get a code object from source."""
source_path = self.source_path(fullname)
@@ -388,15 +391,16 @@
"""
- @module_for_loader
- def load_module(self, module):
- """Load a module from source or bytecode."""
- name = module.__name__
- source_path = self.source_path(name)
- bytecode_path = self.bytecode_path(name)
- # get_code can worry about no viable paths existing.
- module.__file__ = source_path or bytecode_path
- return self._load_module(module)
+ def get_filename(self, fullname):
+ """Return the source or bytecode file path."""
+ path = self.source_path(fullname)
+ if path is not None:
+ return path
+ path = self.bytecode_path(fullname)
+ if path is not None:
+ return path
+ raise ImportError("no source or bytecode path available for "
+ "{0!r}".format(fullname))
def get_code(self, fullname):
"""Get a code object from source or bytecode."""
diff --git a/Lib/importlib/abc.py b/Lib/importlib/abc.py
index 7b89d0b..c912280 100644
--- a/Lib/importlib/abc.py
+++ b/Lib/importlib/abc.py
@@ -76,7 +76,23 @@
InspectLoader.register(machinery.FrozenImporter)
-class PyLoader(_bootstrap.PyLoader, ResourceLoader, InspectLoader):
+class ExecutionLoader(InspectLoader):
+
+ """Abstract base class for loaders that wish to support the execution of
+ modules as scripts.
+
+ This ABC represents one of the optional protocols specified in PEP 302.
+
+ """
+
+ @abc.abstractmethod
+ def get_filename(self, fullname:str) -> str:
+ """Abstract method which should return the value that __file__ is to be
+ set to."""
+ raise NotImplementedError
+
+
+class PyLoader(_bootstrap.PyLoader, ResourceLoader, ExecutionLoader):
"""Abstract base class to assist in loading source code by requiring only
back-end storage methods to be implemented.
diff --git a/Lib/importlib/test/source/test_abc_loader.py b/Lib/importlib/test/source/test_abc_loader.py
index 6465d26..8c69cfd 100644
--- a/Lib/importlib/test/source/test_abc_loader.py
+++ b/Lib/importlib/test/source/test_abc_loader.py
@@ -218,6 +218,21 @@
with util.uncache(name), self.assertRaises(ImportError):
mock.load_module(name)
+ def test_get_filename_with_source_path(self):
+ # get_filename() should return what source_path() returns.
+ name = 'mod'
+ path = os.path.join('path', 'to', 'source')
+ mock = PyLoaderMock({name: path})
+ with util.uncache(name):
+ self.assertEqual(mock.get_filename(name), path)
+
+ def test_get_filename_no_source_path(self):
+ # get_filename() should raise ImportError if source_path returns None.
+ name = 'mod'
+ mock = PyLoaderMock({name: None})
+ with util.uncache(name), self.assertRaises(ImportError):
+ mock.get_filename(name)
+
class PyLoaderGetSourceTests(unittest.TestCase):
@@ -283,6 +298,38 @@
super().test_unloadable()
+class PyPycLoaderInterfaceTests(unittest.TestCase):
+
+ """Test for the interface of importlib.abc.PyPycLoader."""
+
+ def get_filename_check(self, src_path, bc_path, expect):
+ name = 'mod'
+ mock = PyPycLoaderMock({name: src_path}, {name: {'path': bc_path}})
+ with util.uncache(name):
+ assert mock.source_path(name) == src_path
+ assert mock.bytecode_path(name) == bc_path
+ self.assertEqual(mock.get_filename(name), expect)
+
+ def test_filename_with_source_bc(self):
+ # When source and bytecode paths present, return the source path.
+ self.get_filename_check('source_path', 'bc_path', 'source_path')
+
+ def test_filename_with_source_no_bc(self):
+ # With source but no bc, return source path.
+ self.get_filename_check('source_path', None, 'source_path')
+
+ def test_filename_with_no_source_bc(self):
+ # With not source but bc, return the bc path.
+ self.get_filename_check(None, 'bc_path', 'bc_path')
+
+ def test_filename_with_no_source_or_bc(self):
+ # With no source or bc, raise ImportError.
+ name = 'mod'
+ mock = PyPycLoaderMock({name: None}, {name: {'path': None}})
+ with util.uncache(name), self.assertRaises(ImportError):
+ mock.get_filename(name)
+
+
class SkipWritingBytecodeTests(unittest.TestCase):
"""Test that bytecode is properly handled based on
@@ -421,9 +468,9 @@
def test_main():
from test.support import run_unittest
run_unittest(PyLoaderTests, PyLoaderInterfaceTests, PyLoaderGetSourceTests,
- PyPycLoaderTests, SkipWritingBytecodeTests,
- RegeneratedBytecodeTests, BadBytecodeFailureTests,
- MissingPathsTests)
+ PyPycLoaderTests, PyPycLoaderInterfaceTests,
+ SkipWritingBytecodeTests, RegeneratedBytecodeTests,
+ BadBytecodeFailureTests, MissingPathsTests)
if __name__ == '__main__':
diff --git a/Lib/importlib/test/test_abc.py b/Lib/importlib/test/test_abc.py
index 6e09534..5229ba4 100644
--- a/Lib/importlib/test/test_abc.py
+++ b/Lib/importlib/test/test_abc.py
@@ -53,9 +53,15 @@
machinery.FrozenImporter]
+class ExecutionLoader(InheritanceTests, unittest.TestCase):
+
+ superclasses = [abc.InspectLoader]
+ subclasses = [abc.PyLoader]
+
+
class PyLoader(InheritanceTests, unittest.TestCase):
- superclasses = [abc.Loader, abc.ResourceLoader, abc.InspectLoader]
+ superclasses = [abc.Loader, abc.ResourceLoader, abc.ExecutionLoader]
class PyPycLoader(InheritanceTests, unittest.TestCase):
diff --git a/Misc/NEWS b/Misc/NEWS
index 9fa8e96..178255c 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -43,6 +43,11 @@
Library
-------
+- Add importlib.abc.ExecutionLoader to represent the PEP 302 protocol for
+ loaders that allow for modules to be executed. Both importlib.abc.PyLoader
+ and PyPycLoader inherit from this class and provide implementations in
+ relation to other methods required by the ABCs.
+
- importlib.abc.PyLoader did not inherit from importlib.abc.ResourceLoader like
the documentation said it did even though the code in PyLoader relied on the
abstract method required by ResourceLoader.