]> granicus.if.org Git - python/commitdiff
Implement the PEP 302 protocol for get_filename() as
authorBrett Cannon <bcannon@gmail.com>
Mon, 20 Jul 2009 04:23:48 +0000 (04:23 +0000)
committerBrett Cannon <bcannon@gmail.com>
Mon, 20 Jul 2009 04:23:48 +0000 (04:23 +0000)
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
Lib/importlib/_bootstrap.py
Lib/importlib/abc.py
Lib/importlib/test/source/test_abc_loader.py
Lib/importlib/test/test_abc.py
Misc/NEWS

index 7ae696d86b11d11e989a00b0293316451dd7cb76..e051472cd32694f1590486a342b9cdcb42b2ac53 100644 (file)
@@ -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
index ee3f1e6bf23de94e5660ac4e7d5308b98a5e74a4..2c5a1cfc13c626a6d6044c5eefc2602c648657d4 100644 (file)
@@ -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."""
index 7b89d0b4c932057c9a7660875dcdedd5a01635e0..c912280ff1ed4c76dac201f414295abee229805a 100644 (file)
@@ -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.
index 6465d261b7dec721f7bd37b4a085bc1c09e47c09..8c69cfd53715750f258faeedbd33dc5a9bdeb3a0 100644 (file)
@@ -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__':
index 6e09534d8c3b8655218b9ed3e5d3a6e1fe7cbb04..5229ba4309ab679401162d273f73485053ea8fa2 100644 (file)
@@ -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):
index 9fa8e96ac5cee044449fc851a98b05974da0ba71..178255c513dd539aad9d5a56e79117688f62a909 100644 (file)
--- 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.