From 6919427e9462d05f402faa5f846f43e08347cebe Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Mon, 20 Jul 2009 04:23:48 +0000 Subject: 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. --- Doc/library/importlib.rst | 41 ++++++++++++++++++--- Lib/importlib/_bootstrap.py | 38 +++++++++++--------- Lib/importlib/abc.py | 18 +++++++++- Lib/importlib/test/source/test_abc_loader.py | 53 ++++++++++++++++++++++++++-- Lib/importlib/test/test_abc.py | 8 ++++- Misc/NEWS | 5 +++ 6 files changed, 136 insertions(+), 27 deletions(-) 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 @@ 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 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 @@ 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.""" 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.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. 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 @@ 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__': 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 @@ 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): diff --git a/Misc/NEWS b/Misc/NEWS index 9fa8e96..178255c 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -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. -- cgit v0.12