mirror of
https://github.com/python/cpython.git
synced 2025-08-03 16:39:00 +00:00
Introduce importlib.abc. The module contains various ABCs related to imports
(mostly stuff specified by PEP 302). There are two ABCs, PyLoader and PyPycLoader, which help with implementing source and source/bytecode loaders by implementing load_module in terms of other methods. This removes a lot of gritty details loaders typically have to worry about.
This commit is contained in:
parent
aa1c8d8899
commit
2a922ed6ad
9 changed files with 739 additions and 171 deletions
|
@ -82,6 +82,175 @@ Functions
|
|||
occuring from to already be imported (i.e., *package* must already be
|
||||
imported).
|
||||
|
||||
:mod:`importlib.abc` -- Abstract base classes related to import
|
||||
---------------------------------------------------------------
|
||||
|
||||
.. module:: importlib.abc
|
||||
:synopsis: Abstract base classes related to import
|
||||
|
||||
The :mod:`importlib.abc` module contains all of the core abstract base classes
|
||||
used by :keyword:`import`. Some subclasses of the core abstract base classes
|
||||
are also provided to help in implementing the core ABCs.
|
||||
|
||||
|
||||
.. class:: Finder
|
||||
|
||||
An abstract base class representing a :term:`finder`.
|
||||
|
||||
..method:: find_module(fullname, path=None)
|
||||
|
||||
An abstract method for finding a :term:`loader` for the specified
|
||||
module. If the :term:`finder` is found on :data:`sys.meta_path` and the
|
||||
module to be searched for is a subpackage or module then *path* is set
|
||||
to the value of :attr:`__path__` from the parent package. If a loader
|
||||
cannot be found, :keyword:`None` is returned.
|
||||
|
||||
The exact definition of a :term:`finder` can be found in :pep:`302`.
|
||||
|
||||
|
||||
.. class:: Loader
|
||||
|
||||
An abstract base class for a :term:`loader`.
|
||||
|
||||
..method:: load_module(fullname)
|
||||
|
||||
An abstract method for loading a module. If the module cannot be
|
||||
loaded, :exc:`ImportError` is raised, otherwise the loaded module is
|
||||
returned.
|
||||
|
||||
If the requested module is already exists in :data:`sys.modules`, that
|
||||
module should be used and reloaded.
|
||||
Otherwise a new module is to be created by the loader and inserted into
|
||||
:data:`sys.modules`before any loading begins to prevent recursion from
|
||||
the import. If the loader inserted into a module and the load fails it
|
||||
must be removed by the loader from :data:`sys.modules`; modules already
|
||||
in :data:`sys.modules` before the loader began execution should be left
|
||||
alone. The :func:`importlib.util.module_for_loader` decorator handles
|
||||
all of these details.
|
||||
|
||||
The loader is expected to set several attributes on the module when
|
||||
adding a new module to :data:`sys.modules`.
|
||||
|
||||
- :attr:`__name__`
|
||||
The name of the module.
|
||||
|
||||
- :attr:`__file__`
|
||||
The path to where the module data is stored (not set for built-in
|
||||
modules).
|
||||
|
||||
- :attr:`__path__`
|
||||
Set to a list of strings specifying the search path within a
|
||||
package. This attribute is not set on modules.
|
||||
|
||||
- :attr:`__package__`
|
||||
The parent package for the module/package. If the module is
|
||||
top-level then it has a value of the empty string. The
|
||||
:func:`importlib.util.set_package` decorator can handle the details
|
||||
for :attr:`__package__`.
|
||||
|
||||
- :attr:`__loader__`
|
||||
Set to the loader used to load the module.
|
||||
|
||||
See :pep:`302` for the exact definition for a loader.
|
||||
|
||||
|
||||
.. class:: ResourceLoader
|
||||
|
||||
An abstract base class for a :term:`loader` which implements the optional
|
||||
:pep:`302` protocol for loading arbitrary resources from the storage
|
||||
back-end.
|
||||
|
||||
..method:: get_data(path)
|
||||
|
||||
An abstract method to return the bytes for the data located at *path*.
|
||||
Loaders that have a file-like storage back-end can implement this
|
||||
abstract method to give direct access
|
||||
to the data stored. :exc:`IOError` is to be raised if the *path* cannot
|
||||
be found. The *path* is expected to be constructed using a module's
|
||||
:attr:`__path__` attribute or an item from :attr:`__path__`.
|
||||
|
||||
|
||||
.. class:: InspectLoader
|
||||
|
||||
An abstract base class for a :term:`loader` which implements the optional
|
||||
:pep:`302` protocol for loaders which inspect modules.
|
||||
|
||||
..method:: is_package(fullname)
|
||||
|
||||
An abstract method to return a true value if the module is a package, a
|
||||
false value otherwise. :exc:`ImportError` is raised if the
|
||||
:term:`loader` cannot find the module.
|
||||
|
||||
..method:: get_source(fullname)
|
||||
|
||||
An abstract method to return the source of a module. It is returned as
|
||||
a string with universal newline support. Returns :keyword:`None` if no
|
||||
source is available (e.g. a built-in module). Raises :exc:`ImportError`
|
||||
if the loader cannot find the module specified.
|
||||
|
||||
..method:: get_code(fullname)
|
||||
|
||||
An abstract method to return the :class:`code` object for a module.
|
||||
:keyword:`None` is returned if the module does not have a code object
|
||||
(e.g. built-in module). :exc:`ImportError` is raised if loader cannot
|
||||
find the requested module.
|
||||
|
||||
|
||||
.. class:: PyLoader
|
||||
|
||||
An abstract base class inheriting from :class:`importlib.abc.InspectLoader`
|
||||
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
|
||||
code is stored; all other details for loading Python source code will be
|
||||
handled by the concrete implementations of key methods.
|
||||
|
||||
..method:: source_path(fullname)
|
||||
|
||||
An abstract method that returns the path to the source code for a
|
||||
module. Should return :keyword:`None` if there is no source code.
|
||||
:exc:`ImportError` if the module cannot be found.
|
||||
|
||||
..method:: load_module(fullname)
|
||||
|
||||
A concrete implementation of :meth:`importlib.abc.Loader.load_module`
|
||||
that loads Python source code.
|
||||
|
||||
..method:: get_code(fullname)
|
||||
|
||||
A concrete implementation of
|
||||
:meth:`importlib.abc.InspectLoader.get_code` that creates code objects
|
||||
from Python source code.
|
||||
|
||||
|
||||
.. class:: PyPycLoader
|
||||
|
||||
An abstract base class inheriting from :class:`importlib.abc.PyLoader`.
|
||||
This ABC is meant to help in creating loaders that support both Python
|
||||
source and bytecode.
|
||||
|
||||
..method:: source_mtime(fullname)
|
||||
|
||||
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
|
||||
module cannot be found then :exc:`ImportError` is raised.
|
||||
|
||||
..method:: bytecode_path(fullname)
|
||||
|
||||
An abstract method which returns the path to the bytecode for the
|
||||
specified module. :keyword:`None` is returned if there is no bytecode.
|
||||
:exc:`ImportError` is raised if the module is not found.
|
||||
|
||||
..method:: write_bytecode(fullname, bytecode)
|
||||
|
||||
An abstract method which has the loader write *bytecode* for future
|
||||
use. If the bytecode is written, return :keyword:`True`. Return
|
||||
:keyword:`False` if the bytecode could not be written. This method
|
||||
should not be called if :data:`sys.dont_write_bytecode` is true.
|
||||
|
||||
|
||||
:mod:`importlib.machinery` -- Importers and path hooks
|
||||
------------------------------------------------------
|
||||
|
||||
|
@ -93,44 +262,27 @@ find and load modules.
|
|||
|
||||
.. class:: BuiltinImporter
|
||||
|
||||
:term:`Importer` for built-in modules. All known built-in modules are
|
||||
listed in :data:`sys.builtin_module_names`.
|
||||
An :term:`importer` for built-in modules. All known built-in modules are
|
||||
listed in :data:`sys.builtin_module_names`. This class implements the
|
||||
:class:`importlib.abc.Finder` and :class:`importlib.abc.Loader` ABCs.
|
||||
|
||||
Only class methods are defined by this class to alleviate the need for
|
||||
instantiation.
|
||||
|
||||
.. classmethod:: find_module(fullname, path=None)
|
||||
|
||||
Class method that allows this class to be a :term:`finder` for built-in
|
||||
modules.
|
||||
|
||||
.. classmethod:: load_module(fullname)
|
||||
|
||||
Class method that allows this class to be a :term:`loader` for built-in
|
||||
modules.
|
||||
|
||||
|
||||
.. class:: FrozenImporter
|
||||
|
||||
:term:`Importer` for frozen modules.
|
||||
An :term:`importer` for frozen modules. This class implements the
|
||||
:class:`importlib.abc.Finder` and :class:`importlib.abc.Loader` ABCs.
|
||||
|
||||
Only class methods are defined by this class to alleviate the need for
|
||||
instantiation.
|
||||
|
||||
.. classmethod:: find_module(fullname, path=None)
|
||||
|
||||
Class method that allows this class to be a :term:`finder` for frozen
|
||||
modules.
|
||||
|
||||
.. classmethod:: load_module(fullname)
|
||||
|
||||
Class method that allows this class to be a :term:`loader` for frozen
|
||||
modules.
|
||||
|
||||
|
||||
.. class:: PathFinder
|
||||
|
||||
:term:`Finder` for :data:`sys.path`.
|
||||
:term:`Finder` for :data:`sys.path`. This class implements the
|
||||
:class:`importlib.abc.Finder` ABC.
|
||||
|
||||
This class does not perfectly mirror the semantics of :keyword:`import` in
|
||||
terms of :data:`sys.path`. No implicit path hooks are assumed for
|
||||
|
@ -142,15 +294,15 @@ find and load modules.
|
|||
.. classmethod:: find_module(fullname, path=None)
|
||||
|
||||
Class method that attempts to find a :term:`loader` for the module
|
||||
specified by *fullname* either on :data:`sys.path` or, if defined, on
|
||||
specified by *fullname* on :data:`sys.path` or, if defined, on
|
||||
*path*. For each path entry that is searched,
|
||||
:data:`sys.path_importer_cache` is checked. If an non-false object is
|
||||
found then it is used as the :term:`finder` to query for the module
|
||||
being searched for. For no entry is found in
|
||||
found then it is used as the :term:`finder` to look for the module
|
||||
being searched for. If no entry is found in
|
||||
:data:`sys.path_importer_cache`, then :data:`sys.path_hooks` is
|
||||
searched for a finder for the path entry and, if found, is stored in
|
||||
:data:`sys.path_importer_cache` along with being queried about the
|
||||
module.
|
||||
module. If no finder is ever found then :keyword:`None` is returned.
|
||||
|
||||
|
||||
:mod:`importlib.util` -- Utility code for importers
|
||||
|
@ -166,10 +318,11 @@ an :term:`importer`.
|
|||
|
||||
A :term:`decorator` for a :term:`loader` which handles selecting the proper
|
||||
module object to load with. The decorated method is expected to have a call
|
||||
signature of ``method(self, module_object)`` for which the second argument
|
||||
will be the module object to be used by the loader (note that the decorator
|
||||
signature taking two positional arguments
|
||||
(e.g. ``load_module(self, module)``) for which the second argument
|
||||
will be the module object to be used by the loader. Note that the decorator
|
||||
will not work on static methods because of the assumption of two
|
||||
arguments).
|
||||
arguments.
|
||||
|
||||
The decorated method will take in the name of the module to be loaded as
|
||||
expected for a :term:`loader`. If the module is not found in
|
||||
|
|
|
@ -3,42 +3,12 @@ to do
|
|||
|
||||
* Public API left to expose (w/ docs!)
|
||||
|
||||
+ abc
|
||||
+ abc.PyLoader.get_source
|
||||
+ util.set_loader
|
||||
|
||||
- Finder
|
||||
* Implement InspectLoader for BuiltinImporter and FrozenImporter.
|
||||
|
||||
* find_module
|
||||
|
||||
- Loader
|
||||
|
||||
* load_module
|
||||
|
||||
- ResourceLoader(Loader)
|
||||
|
||||
* get_data
|
||||
|
||||
- InspectLoader(Loader)
|
||||
|
||||
* is_package
|
||||
* get_code
|
||||
* get_source
|
||||
|
||||
- PyLoader(ResourceLoader)
|
||||
|
||||
* source_path
|
||||
|
||||
- PyPycLoader(PyLoader)
|
||||
|
||||
* source_mtime
|
||||
* bytecode_path
|
||||
* write_bytecode
|
||||
|
||||
+ test (Really want to worry about compatibility with future versions?)
|
||||
|
||||
- abc
|
||||
|
||||
* FinderTests [doc]
|
||||
* LoaderTests [doc]
|
||||
+ Expose function to see if a frozen module is a package.
|
||||
|
||||
* Remove ``import *`` from importlib.__init__.
|
||||
|
||||
|
@ -68,3 +38,4 @@ in the language specification).
|
|||
+ imp
|
||||
+ py_compile
|
||||
+ compileall
|
||||
+ zipimport
|
||||
|
|
|
@ -383,14 +383,8 @@ class PyPycLoader(PyLoader):
|
|||
def load_module(self, module):
|
||||
"""Load a module from source or bytecode."""
|
||||
name = module.__name__
|
||||
try:
|
||||
source_path = self.source_path(name)
|
||||
except ImportError:
|
||||
source_path = None
|
||||
try:
|
||||
bytecode_path = self.bytecode_path(name)
|
||||
except ImportError:
|
||||
bytecode_path = None
|
||||
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)
|
||||
|
|
110
Lib/importlib/abc.py
Normal file
110
Lib/importlib/abc.py
Normal file
|
@ -0,0 +1,110 @@
|
|||
"""Abstract base classes related to import."""
|
||||
from . import _bootstrap
|
||||
from . import machinery
|
||||
import abc
|
||||
import types
|
||||
|
||||
|
||||
class Loader(metaclass=abc.ABCMeta):
|
||||
|
||||
"""Abstract base class for import loaders.
|
||||
|
||||
See PEP 302 for details.
|
||||
|
||||
"""
|
||||
|
||||
def load_module(self, fullname:str) -> types.ModuleType:
|
||||
raise NotImplementedError
|
||||
|
||||
Loader.register(machinery.BuiltinImporter)
|
||||
Loader.register(machinery.FrozenImporter)
|
||||
|
||||
|
||||
class Finder(metaclass=abc.ABCMeta):
|
||||
|
||||
"""Abstract base class for import finders.
|
||||
|
||||
See PEP 302 for details.
|
||||
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def find_module(self, fullname:str, path:[str]=None) -> Loader:
|
||||
raise NotImplementedError
|
||||
|
||||
Finder.register(machinery.BuiltinImporter)
|
||||
Finder.register(machinery.FrozenImporter)
|
||||
Finder.register(machinery.PathFinder)
|
||||
|
||||
|
||||
class Importer(Finder, Loader):
|
||||
|
||||
"""Abstract base class for importers."""
|
||||
|
||||
|
||||
|
||||
class ResourceLoader(Loader):
|
||||
|
||||
"""Abstract base class for loaders which can return data from the back-end
|
||||
storage.
|
||||
|
||||
This ABC represents one of the optional protocols specified by PEP 302.
|
||||
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_data(self, path:str) -> bytes:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class InspectLoader(Loader):
|
||||
|
||||
"""Abstract base class for loaders which supports introspection.
|
||||
|
||||
This ABC represents one of the optional protocols specified by PEP 302.
|
||||
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def is_package(self, fullname:str) -> bool:
|
||||
return NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_code(self, fullname:str) -> types.CodeType:
|
||||
return NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_source(self, fullname:str) -> str:
|
||||
return NotImplementedError
|
||||
|
||||
|
||||
class PyLoader(_bootstrap.PyLoader, InspectLoader):
|
||||
|
||||
"""Abstract base class that implements the core parts needed to load Python
|
||||
source code."""
|
||||
|
||||
# load_module and get_code are implemented.
|
||||
|
||||
@abc.abstractmethod
|
||||
def source_path(self, fullname:str) -> object:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class PyPycLoader(_bootstrap.PyPycLoader, PyLoader):
|
||||
|
||||
"""Abstract base class that implements the core parts needed to load Python
|
||||
source and bytecode."""
|
||||
|
||||
# Implements load_module and get_code.
|
||||
|
||||
@abc.abstractmethod
|
||||
def source_mtime(self, fullname:str) -> int:
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def bytecode_path(self, fullname:str) -> object:
|
||||
raise NotImplementedError
|
||||
|
||||
@abc.abstractmethod
|
||||
def write_bytecode(self, fullname:str, bytecode:bytes):
|
||||
raise NotImplementedError
|
|
@ -65,10 +65,11 @@ class LoaderTests(unittest.TestCase, metaclass=abc.ABCMeta):
|
|||
|
||||
Attributes to verify:
|
||||
|
||||
* __file__
|
||||
* __loader__
|
||||
* __name__
|
||||
* __file__
|
||||
* __package__
|
||||
* __path__
|
||||
* __loader__
|
||||
|
||||
"""
|
||||
pass
|
||||
|
|
390
Lib/importlib/test/source/test_abc_loader.py
Normal file
390
Lib/importlib/test/source/test_abc_loader.py
Normal file
|
@ -0,0 +1,390 @@
|
|||
import importlib
|
||||
from importlib import abc
|
||||
from .. import abc as testing_abc
|
||||
from .. import util
|
||||
from . import util as source_util
|
||||
import imp
|
||||
import marshal
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
|
||||
|
||||
class PyLoaderMock(abc.PyLoader):
|
||||
|
||||
# Globals that should be defined for all modules.
|
||||
source = ("_ = '::'.join([__name__, __file__, __package__, "
|
||||
"repr(__loader__)])")
|
||||
|
||||
def __init__(self, data):
|
||||
"""Take a dict of 'module_name: path' pairings.
|
||||
|
||||
Paths should have no file extension, allowing packages to be denoted by
|
||||
ending in '__init__'.
|
||||
|
||||
"""
|
||||
self.module_paths = data
|
||||
self.path_to_module = {val:key for key,val in data.items()}
|
||||
|
||||
def get_data(self, path):
|
||||
if path not in self.path_to_module:
|
||||
raise IOError
|
||||
return self.source.encode('utf-8')
|
||||
|
||||
def is_package(self, name):
|
||||
try:
|
||||
return '__init__' in self.module_paths[name]
|
||||
except KeyError:
|
||||
raise ImportError
|
||||
|
||||
def get_source(self, name): # Should not be needed.
|
||||
raise NotImplementedError
|
||||
|
||||
def source_path(self, name):
|
||||
try:
|
||||
return self.module_paths[name]
|
||||
except KeyError:
|
||||
raise ImportError
|
||||
|
||||
|
||||
class PyPycLoaderMock(abc.PyPycLoader, PyLoaderMock):
|
||||
|
||||
default_mtime = 1
|
||||
|
||||
def __init__(self, source, bc={}):
|
||||
"""Initialize mock.
|
||||
|
||||
'bc' is a dict keyed on a module's name. The value is dict with
|
||||
possible keys of 'path', 'mtime', 'magic', and 'bc'. Except for 'path',
|
||||
each of those keys control if any part of created bytecode is to
|
||||
deviate from default values.
|
||||
|
||||
"""
|
||||
super().__init__(source)
|
||||
self.module_bytecode = {}
|
||||
self.path_to_bytecode = {}
|
||||
self.bytecode_to_path = {}
|
||||
for name, data in bc.items():
|
||||
self.path_to_bytecode[data['path']] = name
|
||||
self.bytecode_to_path[name] = data['path']
|
||||
magic = data.get('magic', imp.get_magic())
|
||||
mtime = importlib._w_long(data.get('mtime', self.default_mtime))
|
||||
if 'bc' in data:
|
||||
bc = data['bc']
|
||||
else:
|
||||
bc = self.compile_bc(name)
|
||||
self.module_bytecode[name] = magic + mtime + bc
|
||||
|
||||
def compile_bc(self, name):
|
||||
source_path = self.module_paths.get(name, '<test>') or '<test>'
|
||||
code = compile(self.source, source_path, 'exec')
|
||||
return marshal.dumps(code)
|
||||
|
||||
def source_mtime(self, name):
|
||||
if name in self.module_paths:
|
||||
return self.default_mtime
|
||||
elif name in self.module_bytecode:
|
||||
return None
|
||||
else:
|
||||
raise ImportError
|
||||
|
||||
def bytecode_path(self, name):
|
||||
try:
|
||||
return self.bytecode_to_path[name]
|
||||
except KeyError:
|
||||
if name in self.module_paths:
|
||||
return None
|
||||
else:
|
||||
raise ImportError
|
||||
|
||||
def write_bytecode(self, name, bytecode):
|
||||
self.module_bytecode[name] = bytecode
|
||||
return True
|
||||
|
||||
def get_data(self, path):
|
||||
if path in self.path_to_module:
|
||||
return super().get_data(path)
|
||||
elif path in self.path_to_bytecode:
|
||||
name = self.path_to_bytecode[path]
|
||||
return self.module_bytecode[name]
|
||||
else:
|
||||
raise IOError
|
||||
|
||||
def is_package(self, name):
|
||||
try:
|
||||
return super().is_package(name)
|
||||
except TypeError:
|
||||
return '__init__' in self.bytecode_to_path[name]
|
||||
|
||||
|
||||
class PyLoaderTests(testing_abc.LoaderTests):
|
||||
|
||||
"""Tests for importlib.abc.PyLoader."""
|
||||
|
||||
mocker = PyLoaderMock
|
||||
|
||||
def eq_attrs(self, ob, **kwargs):
|
||||
for attr, val in kwargs.items():
|
||||
self.assertEqual(getattr(ob, attr), val)
|
||||
|
||||
def test_module(self):
|
||||
name = '<module>'
|
||||
path = 'path/to/module'
|
||||
mock = self.mocker({name: path})
|
||||
with util.uncache(name):
|
||||
module = mock.load_module(name)
|
||||
self.assert_(name in sys.modules)
|
||||
self.eq_attrs(module, __name__=name, __file__=path, __package__='',
|
||||
__loader__=mock)
|
||||
self.assert_(not hasattr(module, '__path__'))
|
||||
return mock, name
|
||||
|
||||
def test_package(self):
|
||||
name = '<pkg>'
|
||||
path = '/path/to/<pkg>/__init__'
|
||||
mock = self.mocker({name: path})
|
||||
with util.uncache(name):
|
||||
module = mock.load_module(name)
|
||||
self.assert_(name in sys.modules)
|
||||
self.eq_attrs(module, __name__=name, __file__=path,
|
||||
__path__=[os.path.dirname(path)], __package__=name,
|
||||
__loader__=mock)
|
||||
return mock, name
|
||||
|
||||
def test_lacking_parent(self):
|
||||
name = 'pkg.mod'
|
||||
path = 'path/to/pkg/mod'
|
||||
mock = self.mocker({name: path})
|
||||
with util.uncache(name):
|
||||
module = mock.load_module(name)
|
||||
self.assert_(name in sys.modules)
|
||||
self.eq_attrs(module, __name__=name, __file__=path, __package__='pkg',
|
||||
__loader__=mock)
|
||||
self.assert_(not hasattr(module, '__path__'))
|
||||
return mock, name
|
||||
|
||||
def test_module_reuse(self):
|
||||
name = 'mod'
|
||||
path = 'path/to/mod'
|
||||
module = imp.new_module(name)
|
||||
mock = self.mocker({name: path})
|
||||
with util.uncache(name):
|
||||
sys.modules[name] = module
|
||||
loaded_module = mock.load_module(name)
|
||||
self.assert_(loaded_module is module)
|
||||
self.assert_(sys.modules[name] is module)
|
||||
return mock, name
|
||||
|
||||
def test_state_after_failure(self):
|
||||
name = "mod"
|
||||
module = imp.new_module(name)
|
||||
module.blah = None
|
||||
mock = self.mocker({name: 'path/to/mod'})
|
||||
mock.source = "1/0"
|
||||
with util.uncache(name):
|
||||
sys.modules[name] = module
|
||||
self.assertRaises(ZeroDivisionError, mock.load_module, name)
|
||||
self.assert_(sys.modules[name] is module)
|
||||
self.assert_(hasattr(module, 'blah'))
|
||||
return mock
|
||||
|
||||
def test_unloadable(self):
|
||||
name = "mod"
|
||||
mock = self.mocker({name: 'path/to/mod'})
|
||||
mock.source = "1/0"
|
||||
with util.uncache(name):
|
||||
self.assertRaises(ZeroDivisionError, mock.load_module, name)
|
||||
self.assert_(name not in sys.modules)
|
||||
return mock
|
||||
|
||||
|
||||
class PyLoaderInterfaceTests(unittest.TestCase):
|
||||
|
||||
|
||||
def test_no_source_path(self):
|
||||
# No source path should lead to ImportError.
|
||||
name = 'mod'
|
||||
mock = PyLoaderMock({})
|
||||
with util.uncache(name):
|
||||
self.assertRaises(ImportError, mock.load_module, name)
|
||||
|
||||
def test_source_path_is_None(self):
|
||||
name = 'mod'
|
||||
mock = PyLoaderMock({name: None})
|
||||
with util.uncache(name):
|
||||
self.assertRaises(ImportError, mock.load_module, name)
|
||||
|
||||
|
||||
class PyPycLoaderTests(PyLoaderTests):
|
||||
|
||||
"""Tests for importlib.abc.PyPycLoader."""
|
||||
|
||||
mocker = PyPycLoaderMock
|
||||
|
||||
@source_util.writes_bytecode
|
||||
def verify_bytecode(self, mock, name):
|
||||
assert name in mock.module_paths
|
||||
self.assert_(name in mock.module_bytecode)
|
||||
magic = mock.module_bytecode[name][:4]
|
||||
self.assertEqual(magic, imp.get_magic())
|
||||
mtime = importlib._r_long(mock.module_bytecode[name][4:8])
|
||||
self.assertEqual(mtime, 1)
|
||||
bc = mock.module_bytecode[name][8:]
|
||||
|
||||
|
||||
def test_module(self):
|
||||
mock, name = super().test_module()
|
||||
self.verify_bytecode(mock, name)
|
||||
|
||||
def test_package(self):
|
||||
mock, name = super().test_package()
|
||||
self.verify_bytecode(mock, name)
|
||||
|
||||
def test_lacking_parent(self):
|
||||
mock, name = super().test_lacking_parent()
|
||||
self.verify_bytecode(mock, name)
|
||||
|
||||
def test_module_reuse(self):
|
||||
mock, name = super().test_module_reuse()
|
||||
self.verify_bytecode(mock, name)
|
||||
|
||||
def test_state_after_failure(self):
|
||||
super().test_state_after_failure()
|
||||
|
||||
def test_unloadable(self):
|
||||
super().test_unloadable()
|
||||
|
||||
|
||||
class SkipWritingBytecodeTests(unittest.TestCase):
|
||||
|
||||
"""Test that bytecode is properly handled based on
|
||||
sys.dont_write_bytecode."""
|
||||
|
||||
@source_util.writes_bytecode
|
||||
def run_test(self, dont_write_bytecode):
|
||||
name = 'mod'
|
||||
mock = PyPycLoaderMock({name: 'path/to/mod'})
|
||||
sys.dont_write_bytecode = dont_write_bytecode
|
||||
with util.uncache(name):
|
||||
mock.load_module(name)
|
||||
self.assert_((name in mock.module_bytecode) is not
|
||||
dont_write_bytecode)
|
||||
|
||||
def test_no_bytecode_written(self):
|
||||
self.run_test(True)
|
||||
|
||||
def test_bytecode_written(self):
|
||||
self.run_test(False)
|
||||
|
||||
|
||||
class RegeneratedBytecodeTests(unittest.TestCase):
|
||||
|
||||
"""Test that bytecode is regenerated as expected."""
|
||||
|
||||
@source_util.writes_bytecode
|
||||
def test_different_magic(self):
|
||||
# A different magic number should lead to new bytecode.
|
||||
name = 'mod'
|
||||
bad_magic = b'\x00\x00\x00\x00'
|
||||
assert bad_magic != imp.get_magic()
|
||||
mock = PyPycLoaderMock({name: 'path/to/mod'},
|
||||
{name: {'path': 'path/to/mod.bytecode',
|
||||
'magic': bad_magic}})
|
||||
with util.uncache(name):
|
||||
mock.load_module(name)
|
||||
self.assert_(name in mock.module_bytecode)
|
||||
magic = mock.module_bytecode[name][:4]
|
||||
self.assertEqual(magic, imp.get_magic())
|
||||
|
||||
@source_util.writes_bytecode
|
||||
def test_old_mtime(self):
|
||||
# Bytecode with an older mtime should be regenerated.
|
||||
name = 'mod'
|
||||
old_mtime = PyPycLoaderMock.default_mtime - 1
|
||||
mock = PyPycLoaderMock({name: 'path/to/mod'},
|
||||
{name: {'path': 'path/to/mod.bytecode', 'mtime': old_mtime}})
|
||||
with util.uncache(name):
|
||||
mock.load_module(name)
|
||||
self.assert_(name in mock.module_bytecode)
|
||||
mtime = importlib._r_long(mock.module_bytecode[name][4:8])
|
||||
self.assertEqual(mtime, PyPycLoaderMock.default_mtime)
|
||||
|
||||
|
||||
class BadBytecodeFailureTests(unittest.TestCase):
|
||||
|
||||
"""Test import failures when there is no source and parts of the bytecode
|
||||
is bad."""
|
||||
|
||||
def test_bad_magic(self):
|
||||
# A bad magic number should lead to an ImportError.
|
||||
name = 'mod'
|
||||
bad_magic = b'\x00\x00\x00\x00'
|
||||
mock = PyPycLoaderMock({}, {name: {'path': 'path/to/mod',
|
||||
'magic': bad_magic}})
|
||||
with util.uncache(name):
|
||||
self.assertRaises(ImportError, mock.load_module, name)
|
||||
|
||||
def test_bad_bytecode(self):
|
||||
# Bad code object bytecode should elad to an ImportError.
|
||||
name = 'mod'
|
||||
mock = PyPycLoaderMock({}, {name: {'path': '/path/to/mod', 'bc': b''}})
|
||||
with util.uncache(name):
|
||||
self.assertRaises(ImportError, mock.load_module, name)
|
||||
|
||||
|
||||
def raise_ImportError(*args, **kwargs):
|
||||
raise ImportError
|
||||
|
||||
class MissingPathsTests(unittest.TestCase):
|
||||
|
||||
"""Test what happens when a source or bytecode path does not exist (either
|
||||
from *_path returning None or raising ImportError)."""
|
||||
|
||||
def test_source_path_None(self):
|
||||
# Bytecode should be used when source_path returns None, along with
|
||||
# __file__ being set to the bytecode path.
|
||||
name = 'mod'
|
||||
bytecode_path = 'path/to/mod'
|
||||
mock = PyPycLoaderMock({name: None}, {name: {'path': bytecode_path}})
|
||||
with util.uncache(name):
|
||||
module = mock.load_module(name)
|
||||
self.assertEqual(module.__file__, bytecode_path)
|
||||
|
||||
# Testing for bytecode_path returning None handled by all tests where no
|
||||
# bytecode initially exists.
|
||||
|
||||
def test_all_paths_None(self):
|
||||
# If all *_path methods return None, raise ImportError.
|
||||
name = 'mod'
|
||||
mock = PyPycLoaderMock({name: None})
|
||||
with util.uncache(name):
|
||||
self.assertRaises(ImportError, mock.load_module, name)
|
||||
|
||||
def test_source_path_ImportError(self):
|
||||
# An ImportError from source_path should trigger an ImportError.
|
||||
name = 'mod'
|
||||
mock = PyPycLoaderMock({}, {name: {'path': 'path/to/mod'}})
|
||||
with util.uncache(name):
|
||||
self.assertRaises(ImportError, mock.load_module, name)
|
||||
|
||||
def test_bytecode_path_ImportError(self):
|
||||
# An ImportError from bytecode_path should trigger an ImportError.
|
||||
name = 'mod'
|
||||
mock = PyPycLoaderMock({name: 'path/to/mod'})
|
||||
bad_meth = types.MethodType(raise_ImportError, mock)
|
||||
mock.bytecode_path = bad_meth
|
||||
with util.uncache(name):
|
||||
self.assertRaises(ImportError, mock.load_module, name)
|
||||
|
||||
|
||||
def test_main():
|
||||
from test.support import run_unittest
|
||||
run_unittest(PyLoaderTests, PyLoaderInterfaceTests,
|
||||
PyPycLoaderTests, SkipWritingBytecodeTests,
|
||||
RegeneratedBytecodeTests, BadBytecodeFailureTests,
|
||||
MissingPathsTests)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_main()
|
|
@ -102,102 +102,6 @@ class SimpleTest(unittest.TestCase):
|
|||
self.assert_('_temp' not in sys.modules)
|
||||
|
||||
|
||||
class DontWriteBytecodeTest(unittest.TestCase):
|
||||
|
||||
"""If sys.dont_write_bytcode is true then no bytecode should be created."""
|
||||
|
||||
def tearDown(self):
|
||||
sys.dont_write_bytecode = False
|
||||
|
||||
@source_util.writes_bytecode
|
||||
def run_test(self, assertion):
|
||||
with source_util.create_modules('_temp') as mapping:
|
||||
loader = importlib.PyPycFileLoader('_temp', mapping['_temp'], False)
|
||||
loader.load_module('_temp')
|
||||
bytecode_path = source_util.bytecode_path(mapping['_temp'])
|
||||
assertion(bytecode_path)
|
||||
|
||||
def test_bytecode_written(self):
|
||||
fxn = lambda bc_path: self.assert_(os.path.exists(bc_path))
|
||||
self.run_test(fxn)
|
||||
|
||||
def test_bytecode_not_written(self):
|
||||
sys.dont_write_bytecode = True
|
||||
fxn = lambda bc_path: self.assert_(not os.path.exists(bc_path))
|
||||
self.run_test(fxn)
|
||||
|
||||
|
||||
class BadDataTest(unittest.TestCase):
|
||||
|
||||
"""If the bytecode has a magic number that does not match the
|
||||
interpreters', ImportError is raised [bad magic]. The timestamp can have
|
||||
any value. And bad marshal data raises ValueError.
|
||||
|
||||
"""
|
||||
|
||||
# [bad magic]
|
||||
def test_bad_magic(self):
|
||||
with source_util.create_modules('_temp') as mapping:
|
||||
py_compile.compile(mapping['_temp'])
|
||||
os.unlink(mapping['_temp'])
|
||||
bytecode_path = source_util.bytecode_path(mapping['_temp'])
|
||||
with open(bytecode_path, 'r+b') as file:
|
||||
file.seek(0)
|
||||
file.write(b'\x00\x00\x00\x00')
|
||||
loader = importlib.PyPycFileLoader('_temp', mapping['_temp'], False)
|
||||
self.assertRaises(ImportError, loader.load_module, '_temp')
|
||||
self.assert_('_temp' not in sys.modules)
|
||||
|
||||
|
||||
class SourceBytecodeInteraction(unittest.TestCase):
|
||||
|
||||
"""When both source and bytecode are present, certain rules dictate which
|
||||
version of the code takes precedent. All things being equal, the bytecode
|
||||
is used with the value of __file__ set to the source [basic top-level],
|
||||
[basic package], [basic sub-module], [basic sub-package].
|
||||
|
||||
"""
|
||||
|
||||
def import_(self, file, module, *, pkg=False):
|
||||
loader = importlib.PyPycFileLoader(module, file, pkg)
|
||||
return loader.load_module(module)
|
||||
|
||||
def run_test(self, test, *create, pkg=False):
|
||||
create += (test,)
|
||||
with source_util.create_modules(*create) as mapping:
|
||||
for name in create:
|
||||
py_compile.compile(mapping[name])
|
||||
if pkg:
|
||||
import_name = test.rsplit('.', 1)[0]
|
||||
else:
|
||||
import_name = test
|
||||
loader = importlib.PyPycFileLoader(import_name, mapping[test], pkg)
|
||||
# Because some platforms only have a granularity to the second for
|
||||
# atime you can't check the physical files. Instead just make it an
|
||||
# exception trigger if source was read.
|
||||
loader.get_source = lambda self, x: 42
|
||||
module = loader.load_module(import_name)
|
||||
self.assertEqual(module.__file__, mapping[name])
|
||||
self.assert_(import_name in sys.modules)
|
||||
self.assertEqual(id(module), id(sys.modules[import_name]))
|
||||
|
||||
# [basic top-level]
|
||||
def test_basic_top_level(self):
|
||||
self.run_test('top_level')
|
||||
|
||||
# [basic package]
|
||||
def test_basic_package(self):
|
||||
self.run_test('pkg.__init__', pkg=True)
|
||||
|
||||
# [basic sub-module]
|
||||
def test_basic_sub_module(self):
|
||||
self.run_test('pkg.sub', 'pkg.__init__')
|
||||
|
||||
# [basic sub-package]
|
||||
def test_basic_sub_package(self):
|
||||
self.run_test('pkg.sub.__init__', 'pkg.__init__', pkg=True)
|
||||
|
||||
|
||||
class BadBytecodeTest(unittest.TestCase):
|
||||
|
||||
"""But there are several things about the bytecode which might lead to the
|
|
@ -1,5 +1,6 @@
|
|||
from .. import util
|
||||
import contextlib
|
||||
import functools
|
||||
import imp
|
||||
import os
|
||||
import os.path
|
||||
|
@ -9,11 +10,24 @@ from test import support
|
|||
|
||||
|
||||
def writes_bytecode(fxn):
|
||||
"""Decorator to protect sys.dont_write_bytecode from mutation."""
|
||||
@functools.wraps(fxn)
|
||||
def wrapper(*args, **kwargs):
|
||||
original = sys.dont_write_bytecode
|
||||
sys.dont_write_bytecode = False
|
||||
to_return = fxn(*args, **kwargs)
|
||||
sys.dont_write_bytecode = original
|
||||
return to_return
|
||||
return wrapper
|
||||
|
||||
|
||||
def writes_bytecode_files(fxn):
|
||||
"""Decorator that returns the function if writing bytecode is enabled, else
|
||||
a stub function that accepts anything and simply returns None."""
|
||||
if sys.dont_write_bytecode:
|
||||
return lambda *args, **kwargs: None
|
||||
else:
|
||||
@functools.wraps(fxn)
|
||||
def wrapper(*args, **kwargs):
|
||||
to_return = fxn(*args, **kwargs)
|
||||
sys.dont_write_bytecode = False
|
||||
|
|
31
Lib/importlib/test/test_abc.py
Normal file
31
Lib/importlib/test/test_abc.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
from importlib import abc
|
||||
from importlib import machinery
|
||||
import unittest
|
||||
|
||||
|
||||
class SubclassTests(unittest.TestCase):
|
||||
|
||||
"""Test that the various classes in importlib are subclasses of the
|
||||
expected ABCS."""
|
||||
|
||||
def verify(self, ABC, *classes):
|
||||
"""Verify the classes are subclasses of the ABC."""
|
||||
for cls in classes:
|
||||
self.assert_(issubclass(cls, ABC))
|
||||
|
||||
def test_Finder(self):
|
||||
self.verify(abc.Finder, machinery.BuiltinImporter,
|
||||
machinery.FrozenImporter, machinery.PathFinder)
|
||||
|
||||
def test_Loader(self):
|
||||
self.verify(abc.Loader, machinery.BuiltinImporter,
|
||||
machinery.FrozenImporter)
|
||||
|
||||
|
||||
def test_main():
|
||||
from test.support import run_unittest
|
||||
run_unittest(SubclassTests)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_main()
|
Loading…
Add table
Add a link
Reference in a new issue