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. ...@@ -202,10 +202,24 @@ are also provided to help in implementing the core ABCs.
:term:`loader` cannot find the module. :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 .. class:: PyLoader
An abstract base class inheriting from :class:`importlib.abc.InspectLoader` An abstract base class inheriting from
and :class:`importlib.abc.ResourceLoader` designed to ease the loading of :class:`importlib.abc.ExecutionLoader` and
:class:`importlib.abc.ResourceLoader` designed to ease the loading of
Python source modules (bytecode is not handled; see Python source modules (bytecode is not handled; see
:class:`importlib.abc.PyPycLoader` for a source/bytecode ABC). A subclass :class:`importlib.abc.PyPycLoader` for a source/bytecode ABC). A subclass
implementing this ABC will only need to worry about exposing how the source 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. ...@@ -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. module. Should return :keyword:`None` if there is no source code.
:exc:`ImportError` if the module cannot be found. :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) .. method:: load_module(fullname)
A concrete implementation of :meth:`importlib.abc.Loader.load_module` A concrete implementation of :meth:`importlib.abc.Loader.load_module`
...@@ -238,8 +259,8 @@ are also provided to help in implementing the core ABCs. ...@@ -238,8 +259,8 @@ are also provided to help in implementing the core ABCs.
A concrete implementation of A concrete implementation of
:meth:`importlib.abc.InspectLoader.get_source`. Uses :meth:`importlib.abc.InspectLoader.get_source`. Uses
:meth:`importlib.abc.InspectLoader.get_data` and :meth:`source_path` to :meth:`importlib.abc.ResourceLoader.get_data` and :meth:`source_path`
get the source code. It tries to guess the source encoding using to get the source code. It tries to guess the source encoding using
:func:`tokenize.detect_encoding`. :func:`tokenize.detect_encoding`.
...@@ -253,7 +274,7 @@ are also provided to help in implementing the core ABCs. ...@@ -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 An abstract method which returns the modification time for the source
code of the specified module. The modification time should be an 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. module cannot be found then :exc:`ImportError` is raised.
.. method:: bytecode_path(fullname) .. method:: bytecode_path(fullname)
...@@ -263,6 +284,16 @@ are also provided to help in implementing the core ABCs. ...@@ -263,6 +284,16 @@ are also provided to help in implementing the core ABCs.
if no bytecode exists (yet). if no bytecode exists (yet).
Raises :exc:`ImportError` if the module is not found. 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) .. method:: write_bytecode(fullname, bytecode)
An abstract method which has the loader write *bytecode* for future An abstract method which has the loader write *bytecode* for future
......
...@@ -315,16 +315,10 @@ class PyLoader: ...@@ -315,16 +315,10 @@ class PyLoader:
@module_for_loader @module_for_loader
def load_module(self, module): def load_module(self, module):
"""Load a source module.""" """Initialize the module."""
return self._load_module(module)
def _load_module(self, module):
"""Initialize a module from source."""
name = module.__name__ name = module.__name__
code_object = self.get_code(module.__name__) code_object = self.get_code(module.__name__)
# __file__ may have been set by the caller, e.g. bytecode path. module.__file__ = self.get_filename(name)
if not hasattr(module, '__file__'):
module.__file__ = self.source_path(name)
if self.is_package(name): if self.is_package(name):
module.__path__ = [module.__file__.rsplit(path_sep, 1)[0]] module.__path__ = [module.__file__.rsplit(path_sep, 1)[0]]
module.__package__ = module.__name__ module.__package__ = module.__name__
...@@ -334,6 +328,15 @@ class PyLoader: ...@@ -334,6 +328,15 @@ class PyLoader:
exec(code_object, module.__dict__) exec(code_object, module.__dict__)
return module 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): def get_code(self, fullname):
"""Get a code object from source.""" """Get a code object from source."""
source_path = self.source_path(fullname) source_path = self.source_path(fullname)
...@@ -388,15 +391,16 @@ class PyPycLoader(PyLoader): ...@@ -388,15 +391,16 @@ class PyPycLoader(PyLoader):
""" """
@module_for_loader def get_filename(self, fullname):
def load_module(self, module): """Return the source or bytecode file path."""
"""Load a module from source or bytecode.""" path = self.source_path(fullname)
name = module.__name__ if path is not None:
source_path = self.source_path(name) return path
bytecode_path = self.bytecode_path(name) path = self.bytecode_path(fullname)
# get_code can worry about no viable paths existing. if path is not None:
module.__file__ = source_path or bytecode_path return path
return self._load_module(module) raise ImportError("no source or bytecode path available for "
"{0!r}".format(fullname))
def get_code(self, fullname): def get_code(self, fullname):
"""Get a code object from source or bytecode.""" """Get a code object from source or bytecode."""
......
...@@ -76,7 +76,23 @@ InspectLoader.register(machinery.BuiltinImporter) ...@@ -76,7 +76,23 @@ InspectLoader.register(machinery.BuiltinImporter)
InspectLoader.register(machinery.FrozenImporter) 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 """Abstract base class to assist in loading source code by requiring only
back-end storage methods to be implemented. back-end storage methods to be implemented.
......
...@@ -218,6 +218,21 @@ class PyLoaderInterfaceTests(unittest.TestCase): ...@@ -218,6 +218,21 @@ class PyLoaderInterfaceTests(unittest.TestCase):
with util.uncache(name), self.assertRaises(ImportError): with util.uncache(name), self.assertRaises(ImportError):
mock.load_module(name) 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): class PyLoaderGetSourceTests(unittest.TestCase):
...@@ -283,6 +298,38 @@ class PyPycLoaderTests(PyLoaderTests): ...@@ -283,6 +298,38 @@ class PyPycLoaderTests(PyLoaderTests):
super().test_unloadable() 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): class SkipWritingBytecodeTests(unittest.TestCase):
"""Test that bytecode is properly handled based on """Test that bytecode is properly handled based on
...@@ -421,9 +468,9 @@ class MissingPathsTests(unittest.TestCase): ...@@ -421,9 +468,9 @@ class MissingPathsTests(unittest.TestCase):
def test_main(): def test_main():
from test.support import run_unittest from test.support import run_unittest
run_unittest(PyLoaderTests, PyLoaderInterfaceTests, PyLoaderGetSourceTests, run_unittest(PyLoaderTests, PyLoaderInterfaceTests, PyLoaderGetSourceTests,
PyPycLoaderTests, SkipWritingBytecodeTests, PyPycLoaderTests, PyPycLoaderInterfaceTests,
RegeneratedBytecodeTests, BadBytecodeFailureTests, SkipWritingBytecodeTests, RegeneratedBytecodeTests,
MissingPathsTests) BadBytecodeFailureTests, MissingPathsTests)
if __name__ == '__main__': if __name__ == '__main__':
......
...@@ -53,9 +53,15 @@ class InspectLoader(InheritanceTests, unittest.TestCase): ...@@ -53,9 +53,15 @@ class InspectLoader(InheritanceTests, unittest.TestCase):
machinery.FrozenImporter] machinery.FrozenImporter]
class ExecutionLoader(InheritanceTests, unittest.TestCase):
superclasses = [abc.InspectLoader]
subclasses = [abc.PyLoader]
class PyLoader(InheritanceTests, unittest.TestCase): class PyLoader(InheritanceTests, unittest.TestCase):
superclasses = [abc.Loader, abc.ResourceLoader, abc.InspectLoader] superclasses = [abc.Loader, abc.ResourceLoader, abc.ExecutionLoader]
class PyPycLoader(InheritanceTests, unittest.TestCase): class PyPycLoader(InheritanceTests, unittest.TestCase):
......
...@@ -43,6 +43,11 @@ C-API ...@@ -43,6 +43,11 @@ C-API
Library 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 - 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 the documentation said it did even though the code in PyLoader relied on the
abstract method required by ResourceLoader. 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