Commit 6919427e authored by Brett Cannon's avatar Brett Cannon

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.
parent 64ef00fa
......@@ -202,10 +202,24 @@ are also provided to help in implementing the core ABCs.
: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 @@ are also provided to help in implementing the core ABCs.
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 @@ are also provided to help in implementing the core ABCs.
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 @@ are also provided to help in implementing the core ABCs.
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 @@ are also provided to help in implementing the core ABCs.
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
......
......@@ -315,16 +315,10 @@ class PyLoader:
@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 @@ class PyLoader:
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 @@ class PyPycLoader(PyLoader):
"""
@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."""
......
......@@ -76,7 +76,23 @@ InspectLoader.register(machinery.BuiltinImporter)
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.
......
......@@ -218,6 +218,21 @@ class PyLoaderInterfaceTests(unittest.TestCase):
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 @@ class PyPycLoaderTests(PyLoaderTests):
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 @@ class MissingPathsTests(unittest.TestCase):
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__':
......
......@@ -53,9 +53,15 @@ class InspectLoader(InheritanceTests, unittest.TestCase):
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):
......
......@@ -43,6 +43,11 @@ C-API
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.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment