Issues #18088, 18089: Introduce

importlib.abc.Loader.init_module_attrs() and implement
importlib.abc.InspectLoader.load_module().

The importlib.abc.Loader.init_module_attrs() method sets the various
attributes on the module being loaded. It is done unconditionally to
support reloading. Typically people used
importlib.util.module_for_loader, but since that's a decorator there
was no way to override it's actions, so init_module_attrs() came into
existence to allow for overriding. This is also why module_for_loader
is now pending deprecation (having its other use replaced by
importlib.util.module_to_load).

All of this allowed for importlib.abc.InspectLoader.load_module() to
be implemented. At this point you can now implement a loader with
nothing more than get_code() (which only requires get_source();
package support requires is_package()). Thanks to init_module_attrs()
the implementation of load_module() is basically a context manager
containing 2 methods calls, a call to exec(), and a return statement.
This commit is contained in:
Brett Cannon 2013-05-31 18:56:47 -04:00
parent f1d7b11db9
commit 0dbb4c7f13
10 changed files with 3934 additions and 3637 deletions

View file

@ -246,7 +246,7 @@ ABC hierarchy::
The loader should set several attributes on the module. The loader should set several attributes on the module.
(Note that some of these attributes can change when a module is (Note that some of these attributes can change when a module is
reloaded.) reloaded; see :meth:`init_module_attrs`):
- :attr:`__name__` - :attr:`__name__`
The name of the module. The name of the module.
@ -289,6 +289,17 @@ ABC hierarchy::
.. versionchanged:: 3.4 .. versionchanged:: 3.4
Made optional instead of an abstractmethod. Made optional instead of an abstractmethod.
.. method:: init_module_attrs(module)
Set the :attr:`__loader__` attribute on the module.
Subclasses overriding this method should set whatever appropriate
attributes it can, getting the module's name from :attr:`__name__` when
needed. All values should also be overridden so that reloading works as
expected.
.. versionadded:: 3.4
.. class:: ResourceLoader .. class:: ResourceLoader
@ -363,6 +374,18 @@ ABC hierarchy::
.. versionadded:: 3.4 .. versionadded:: 3.4
.. method:: init_module_attrs(module)
Set the :attr:`__package__` attribute and :attr:`__path__` attribute to
the empty list if appropriate along with what
:meth:`importlib.abc.Loader.init_module_attrs` sets.
.. versionadded:: 3.4
.. method:: load_module(fullname)
Implementation of :meth:`Loader.load_module`.
.. class:: ExecutionLoader .. class:: ExecutionLoader
@ -383,6 +406,15 @@ ABC hierarchy::
.. versionchanged:: 3.4 .. versionchanged:: 3.4
Raises :exc:`ImportError` instead of :exc:`NotImplementedError`. Raises :exc:`ImportError` instead of :exc:`NotImplementedError`.
.. method:: init_module_attrs(module)
Set :attr:`__file__` and if initializing a package then set
:attr:`__path__` to ``[os.path.dirname(__file__)]`` along with
all attributes set by
:meth:`importlib.abc.InspectLoader.init_module_attrs`.
.. versionadded:: 3.4
.. class:: FileLoader(fullname, path) .. class:: FileLoader(fullname, path)
@ -500,6 +532,14 @@ ABC hierarchy::
``__init__`` when the file extension is removed **and** the module name ``__init__`` when the file extension is removed **and** the module name
itself does not end in ``__init__``. itself does not end in ``__init__``.
.. method:: init_module_attr(module)
Set :attr:`__cached__` using :func:`imp.cache_from_source`. Other
attributes set by
:meth:`importlib.abc.ExecutionLoader.init_module_attrs`.
.. versionadded:: 3.4
:mod:`importlib.machinery` -- Importers and path hooks :mod:`importlib.machinery` -- Importers and path hooks
------------------------------------------------------ ------------------------------------------------------
@ -826,17 +866,18 @@ an :term:`importer`.
module from being in left in :data:`sys.modules`. If the module was already module from being in left in :data:`sys.modules`. If the module was already
in :data:`sys.modules` then it is left alone. in :data:`sys.modules` then it is left alone.
.. note::
:func:`module_to_load` subsumes the module management aspect of this
decorator.
.. versionchanged:: 3.3 .. versionchanged:: 3.3
:attr:`__loader__` and :attr:`__package__` are automatically set :attr:`__loader__` and :attr:`__package__` are automatically set
(when possible). (when possible).
.. versionchanged:: 3.4 .. versionchanged:: 3.4
Set :attr:`__loader__` :attr:`__package__` unconditionally to support Set :attr:`__name__`, :attr:`__loader__` :attr:`__package__`
reloading. unconditionally to support reloading.
.. deprecated:: 3.4
For the benefit of :term:`loader` subclasses, please use
:func:`module_to_load` and
:meth:`importlib.abc.Loader.init_module_attrs` instead.
.. decorator:: set_loader .. decorator:: set_loader
@ -849,7 +890,8 @@ an :term:`importer`.
.. note:: .. note::
As this decorator sets :attr:`__loader__` after loading the module, it is As this decorator sets :attr:`__loader__` after loading the module, it is
recommended to use :func:`module_for_loader` instead when appropriate. recommended to use :meth:`importlib.abc.Loader.init_module_attrs` instead
when appropriate.
.. versionchanged:: 3.4 .. versionchanged:: 3.4
Set ``__loader__`` if set to ``None``, as if the attribute does not Set ``__loader__`` if set to ``None``, as if the attribute does not
@ -862,4 +904,5 @@ an :term:`importer`.
.. note:: .. note::
As this decorator sets :attr:`__package__` after loading the module, it is As this decorator sets :attr:`__package__` after loading the module, it is
recommended to use :func:`module_for_loader` instead when appropriate. recommended to use :meth:`importlib.abc.Loader.init_module_attrs` instead
when appropriate.

View file

@ -232,7 +232,10 @@ Deprecated functions and types of the C API
Deprecated features Deprecated features
------------------- -------------------
* None yet. * :func:`importlib.util.module_for_loader` is pending deprecation. Using
:func:`importlib.util.module_to_load` and
:meth:`importlib.abc.Loader.init_module_attrs` allows subclasses of a loader
to more easily customize module loading.
Porting to Python 3.4 Porting to Python 3.4

View file

@ -538,6 +538,32 @@ def module_to_load(name, *, reset_name=True):
return _ModuleManager(name, reset_name=reset_name) return _ModuleManager(name, reset_name=reset_name)
def _init_package_attrs(loader, module):
"""Set __package__ and __path__ based on what loader.is_package() says."""
name = module.__name__
try:
is_package = loader.is_package(name)
except ImportError:
pass
else:
if is_package:
module.__package__ = name
module.__path__ = []
else:
module.__package__ = name.rpartition('.')[0]
def _init_file_attrs(loader, module):
"""Set __file__ and __path__ based on loader.get_filename()."""
try:
module.__file__ = loader.get_filename(module.__name__)
except ImportError:
pass
else:
if module.__name__ == module.__package__:
module.__path__.append(_path_split(module.__file__)[0])
def set_package(fxn): def set_package(fxn):
"""Set __package__ on the returned module.""" """Set __package__ on the returned module."""
def set_package_wrapper(*args, **kwargs): def set_package_wrapper(*args, **kwargs):
@ -562,42 +588,6 @@ def set_loader(fxn):
return set_loader_wrapper return set_loader_wrapper
def module_for_loader(fxn):
"""Decorator to handle selecting the proper module for loaders.
The decorated function is passed the module to use instead of the module
name. The module passed in to the function is either from sys.modules if
it already exists or is a new module. If the module is new, then __name__
is set the first argument to the method, __loader__ is set to self, and
__package__ is set accordingly (if self.is_package() is defined) will be set
before it is passed to the decorated function (if self.is_package() does
not work for the module it will be set post-load).
If an exception is raised and the decorator created the module it is
subsequently removed from sys.modules.
The decorator assumes that the decorated function takes the module name as
the second argument.
"""
def module_for_loader_wrapper(self, fullname, *args, **kwargs):
with module_to_load(fullname) as module:
module.__loader__ = self
try:
is_package = self.is_package(fullname)
except (ImportError, AttributeError):
pass
else:
if is_package:
module.__package__ = fullname
else:
module.__package__ = fullname.rpartition('.')[0]
# If __package__ was not set above, __import__() will do it later.
return fxn(self, module, *args, **kwargs)
_wrap(module_for_loader_wrapper, fxn)
return module_for_loader_wrapper
def _check_name(method): def _check_name(method):
"""Decorator to verify that the module being requested matches the one the """Decorator to verify that the module being requested matches the one the
loader can handle. loader can handle.
@ -904,24 +894,31 @@ class _LoaderBasics:
tail_name = fullname.rpartition('.')[2] tail_name = fullname.rpartition('.')[2]
return filename_base == '__init__' and tail_name != '__init__' return filename_base == '__init__' and tail_name != '__init__'
@module_for_loader def init_module_attrs(self, module):
def _load_module(self, module, *, sourceless=False): """Set various attributes on the module.
"""Helper for load_module able to handle either source or sourceless
loading.""" ExecutionLoader.init_module_attrs() is used to set __loader__,
name = module.__name__ __package__, __file__, and optionally __path__. The __cached__ attribute
code_object = self.get_code(name) is set using imp.cache_from_source() and __file__.
module.__file__ = self.get_filename(name) """
if not sourceless: module.__loader__ = self # Loader
_init_package_attrs(self, module) # InspectLoader
_init_file_attrs(self, module) # ExecutionLoader
if hasattr(module, '__file__'): # SourceLoader
try: try:
module.__cached__ = cache_from_source(module.__file__) module.__cached__ = cache_from_source(module.__file__)
except NotImplementedError: except NotImplementedError:
module.__cached__ = module.__file__ pass
else:
module.__cached__ = module.__file__ def load_module(self, fullname):
if self.is_package(name): """Load the specified module into sys.modules and return it."""
module.__path__ = [_path_split(module.__file__)[0]] with module_to_load(fullname) as module:
# __package__ and __loader set by @module_for_loader. self.init_module_attrs(module)
_call_with_frames_removed(exec, code_object, module.__dict__) code = self.get_code(fullname)
if code is None:
raise ImportError('cannot load module {!r} when get_code() '
'returns None'.format(fullname))
_call_with_frames_removed(exec, code, module.__dict__)
return module return module
@ -1046,16 +1043,6 @@ class SourceLoader(_LoaderBasics):
pass pass
return code_object return code_object
def load_module(self, fullname):
"""Concrete implementation of Loader.load_module.
Requires ExecutionLoader.get_filename and ResourceLoader.get_data to be
implemented to load source code. Use of bytecode is dictated by whether
get_code uses/writes bytecode.
"""
return self._load_module(fullname)
class FileLoader: class FileLoader:
@ -1133,8 +1120,9 @@ class SourcelessFileLoader(FileLoader, _LoaderBasics):
"""Loader which handles sourceless file imports.""" """Loader which handles sourceless file imports."""
def load_module(self, fullname): def init_module_attrs(self, module):
return self._load_module(fullname, sourceless=True) super().init_module_attrs(module)
module.__cached__ = module.__file__
def get_code(self, fullname): def get_code(self, fullname):
path = self.get_filename(fullname) path = self.get_filename(fullname)
@ -1259,11 +1247,12 @@ class NamespaceLoader:
def module_repr(cls, module): def module_repr(cls, module):
return "<module '{}' (namespace)>".format(module.__name__) return "<module '{}' (namespace)>".format(module.__name__)
@module_for_loader def load_module(self, fullname):
def load_module(self, module):
"""Load a namespace module.""" """Load a namespace module."""
_verbose_message('namespace module loaded with path {!r}', self._path) _verbose_message('namespace module loaded with path {!r}', self._path)
with module_to_load(fullname) as module:
module.__path__ = self._path module.__path__ = self._path
module.__package__ = fullname
return module return module

View file

@ -8,11 +8,6 @@ except ImportError as exc:
raise raise
_frozen_importlib = None _frozen_importlib = None
import abc import abc
import imp
import marshal
import sys
import tokenize
import warnings
def _register(abstract_cls, *classes): def _register(abstract_cls, *classes):
@ -113,6 +108,10 @@ class Loader(metaclass=abc.ABCMeta):
""" """
raise NotImplementedError raise NotImplementedError
def init_module_attrs(self, module):
"""Set the module's __loader__ attribute."""
module.__loader__ = self
class ResourceLoader(Loader): class ResourceLoader(Loader):
@ -177,6 +176,17 @@ class InspectLoader(Loader):
argument should be where the data was retrieved (when applicable).""" argument should be where the data was retrieved (when applicable)."""
return compile(data, path, 'exec', dont_inherit=True) return compile(data, path, 'exec', dont_inherit=True)
def init_module_attrs(self, module):
"""Initialize the __loader__ and __package__ attributes of the module.
The name of the module is gleaned from module.__name__. The __package__
attribute is set based on self.is_package().
"""
super().init_module_attrs(module)
_bootstrap._init_package_attrs(self, module)
load_module = _bootstrap._LoaderBasics.load_module
_register(InspectLoader, machinery.BuiltinImporter, machinery.FrozenImporter, _register(InspectLoader, machinery.BuiltinImporter, machinery.FrozenImporter,
machinery.ExtensionFileLoader) machinery.ExtensionFileLoader)
@ -215,6 +225,18 @@ class ExecutionLoader(InspectLoader):
else: else:
return self.source_to_code(source, path) return self.source_to_code(source, path)
def init_module_attrs(self, module):
"""Initialize the module's attributes.
It is assumed that the module's name has been set on module.__name__.
It is also assumed that any path returned by self.get_filename() uses
(one of) the operating system's path separator(s) to separate filenames
from directories in order to set __path__ intelligently.
InspectLoader.init_module_attrs() sets __loader__ and __package__.
"""
super().init_module_attrs(module)
_bootstrap._init_file_attrs(self, module)
class FileLoader(_bootstrap.FileLoader, ResourceLoader, ExecutionLoader): class FileLoader(_bootstrap.FileLoader, ResourceLoader, ExecutionLoader):

View file

@ -1,11 +1,13 @@
"""Utility code for constructing importers, etc.""" """Utility code for constructing importers, etc."""
from ._bootstrap import module_to_load from ._bootstrap import module_to_load
from ._bootstrap import module_for_loader
from ._bootstrap import set_loader from ._bootstrap import set_loader
from ._bootstrap import set_package from ._bootstrap import set_package
from ._bootstrap import _resolve_name from ._bootstrap import _resolve_name
import functools
import warnings
def resolve_name(name, package): def resolve_name(name, package):
"""Resolve a relative module name to an absolute one.""" """Resolve a relative module name to an absolute one."""
@ -20,3 +22,44 @@ def resolve_name(name, package):
break break
level += 1 level += 1
return _resolve_name(name[level:], package, level) return _resolve_name(name[level:], package, level)
def module_for_loader(fxn):
"""Decorator to handle selecting the proper module for loaders.
The decorated function is passed the module to use instead of the module
name. The module passed in to the function is either from sys.modules if
it already exists or is a new module. If the module is new, then __name__
is set the first argument to the method, __loader__ is set to self, and
__package__ is set accordingly (if self.is_package() is defined) will be set
before it is passed to the decorated function (if self.is_package() does
not work for the module it will be set post-load).
If an exception is raised and the decorator created the module it is
subsequently removed from sys.modules.
The decorator assumes that the decorated function takes the module name as
the second argument.
"""
warnings.warn('To make it easier for subclasses, please use '
'importlib.util.module_to_load() and '
'importlib.abc.Loader.init_module_attrs()',
PendingDeprecationWarning, stacklevel=2)
@functools.wraps(fxn)
def module_for_loader_wrapper(self, fullname, *args, **kwargs):
with module_to_load(fullname) as module:
module.__loader__ = self
try:
is_package = self.is_package(fullname)
except (ImportError, AttributeError):
pass
else:
if is_package:
module.__package__ = fullname
else:
module.__package__ = fullname.rpartition('.')[0]
# If __package__ was not set above, __import__() will do it later.
return fxn(self, module, *args, **kwargs)
return module_for_loader_wrapper

View file

@ -15,7 +15,7 @@ import stat
import sys import sys
import unittest import unittest
from test.support import make_legacy_pyc from test.support import make_legacy_pyc, unload
class SimpleTest(unittest.TestCase): class SimpleTest(unittest.TestCase):
@ -26,23 +26,13 @@ class SimpleTest(unittest.TestCase):
""" """
def test_load_module_API(self): def test_load_module_API(self):
# If fullname is not specified that assume self.name is desired. class Tester(importlib.abc.FileLoader):
class TesterMixin(importlib.abc.Loader): def get_source(self, _): return 'attr = 42'
def load_module(self, fullname): return fullname def is_package(self, _): return False
def module_repr(self, module): return '<module>'
class Tester(importlib.abc.FileLoader, TesterMixin): loader = Tester('blah', 'blah.py')
def get_code(self, _): pass self.addCleanup(unload, 'blah')
def get_source(self, _): pass module = loader.load_module() # Should not raise an exception.
def is_package(self, _): pass
name = 'mod_name'
loader = Tester(name, 'some_path')
self.assertEqual(name, loader.load_module())
self.assertEqual(name, loader.load_module(None))
self.assertEqual(name, loader.load_module(name))
with self.assertRaises(ImportError):
loader.load_module(loader.name + 'XXX')
def test_get_filename_API(self): def test_get_filename_API(self):
# If fullname is not set then assume self.path is desired. # If fullname is not set then assume self.path is desired.
@ -473,13 +463,6 @@ class SourcelessLoaderBadBytecodeTest(BadBytecodeTest):
self._test_non_code_marshal(del_source=True) self._test_non_code_marshal(del_source=True)
def test_main():
from test.support import run_unittest
run_unittest(SimpleTest,
SourceLoaderBadBytecodeTest,
SourcelessLoaderBadBytecodeTest
)
if __name__ == '__main__': if __name__ == '__main__':
test_main() unittest.main()

View file

@ -2,12 +2,14 @@ import importlib
from importlib import abc from importlib import abc
from importlib import machinery from importlib import machinery
import contextlib
import imp import imp
import inspect import inspect
import io import io
import marshal import marshal
import os import os
import sys import sys
from test import support
import unittest import unittest
from unittest import mock from unittest import mock
@ -198,6 +200,15 @@ class ExecutionLoaderDefaultsTests(unittest.TestCase):
with self.assertRaises(ImportError): with self.assertRaises(ImportError):
self.ins.get_filename('blah') self.ins.get_filename('blah')
##### Loader concrete methods ##################################################
class LoaderConcreteMethodTests(unittest.TestCase):
def test_init_module_attrs(self):
loader = LoaderSubclass()
module = imp.new_module('blah')
loader.init_module_attrs(module)
self.assertEqual(module.__loader__, loader)
##### InspectLoader concrete methods ########################################### ##### InspectLoader concrete methods ###########################################
class InspectLoaderSourceToCodeTests(unittest.TestCase): class InspectLoaderSourceToCodeTests(unittest.TestCase):
@ -269,6 +280,93 @@ class InspectLoaderGetCodeTests(unittest.TestCase):
loader.get_code('blah') loader.get_code('blah')
class InspectLoaderInitModuleTests(unittest.TestCase):
@staticmethod
def mock_is_package(return_value):
return mock.patch.object(InspectLoaderSubclass, 'is_package',
return_value=return_value)
def init_module_attrs(self, name):
loader = InspectLoaderSubclass()
module = imp.new_module(name)
loader.init_module_attrs(module)
self.assertEqual(module.__loader__, loader)
return module
def test_package(self):
# If a package, then __package__ == __name__, __path__ == []
with self.mock_is_package(True):
name = 'blah'
module = self.init_module_attrs(name)
self.assertEqual(module.__package__, name)
self.assertEqual(module.__path__, [])
def test_toplevel(self):
# If a module is top-level, __package__ == ''
with self.mock_is_package(False):
name = 'blah'
module = self.init_module_attrs(name)
self.assertEqual(module.__package__, '')
def test_submodule(self):
# If a module is contained within a package then set __package__ to the
# package name.
with self.mock_is_package(False):
name = 'pkg.mod'
module = self.init_module_attrs(name)
self.assertEqual(module.__package__, 'pkg')
def test_is_package_ImportError(self):
# If is_package() raises ImportError, __package__ should be None and
# __path__ should not be set.
with self.mock_is_package(False) as mocked_method:
mocked_method.side_effect = ImportError
name = 'mod'
module = self.init_module_attrs(name)
self.assertIsNone(module.__package__)
self.assertFalse(hasattr(module, '__path__'))
class InspectLoaderLoadModuleTests(unittest.TestCase):
"""Test InspectLoader.load_module()."""
module_name = 'blah'
def setUp(self):
support.unload(self.module_name)
self.addCleanup(support.unload, self.module_name)
def mock_get_code(self):
return mock.patch.object(InspectLoaderSubclass, 'get_code')
def test_get_code_ImportError(self):
# If get_code() raises ImportError, it should propagate.
with self.mock_get_code() as mocked_get_code:
mocked_get_code.side_effect = ImportError
with self.assertRaises(ImportError):
loader = InspectLoaderSubclass()
loader.load_module(self.module_name)
def test_get_code_None(self):
# If get_code() returns None, raise ImportError.
with self.mock_get_code() as mocked_get_code:
mocked_get_code.return_value = None
with self.assertRaises(ImportError):
loader = InspectLoaderSubclass()
loader.load_module(self.module_name)
def test_module_returned(self):
# The loaded module should be returned.
code = compile('attr = 42', '<string>', 'exec')
with self.mock_get_code() as mocked_get_code:
mocked_get_code.return_value = code
loader = InspectLoaderSubclass()
module = loader.load_module(self.module_name)
self.assertEqual(module, sys.modules[self.module_name])
##### ExecutionLoader concrete methods ######################################### ##### ExecutionLoader concrete methods #########################################
class ExecutionLoaderGetCodeTests(unittest.TestCase): class ExecutionLoaderGetCodeTests(unittest.TestCase):
@ -327,6 +425,69 @@ class ExecutionLoaderGetCodeTests(unittest.TestCase):
self.assertEqual(module.attr, 42) self.assertEqual(module.attr, 42)
class ExecutionLoaderInitModuleTests(unittest.TestCase):
@staticmethod
@contextlib.contextmanager
def mock_methods(is_package, filename):
is_package_manager = InspectLoaderInitModuleTests.mock_is_package(is_package)
get_filename_manager = mock.patch.object(ExecutionLoaderSubclass,
'get_filename', return_value=filename)
with is_package_manager as mock_is_package:
with get_filename_manager as mock_get_filename:
yield {'is_package': mock_is_package,
'get_filename': mock_get_filename}
def test_toplevel(self):
# Verify __loader__, __file__, and __package__; no __path__.
name = 'blah'
path = os.path.join('some', 'path', '{}.py'.format(name))
with self.mock_methods(False, path):
loader = ExecutionLoaderSubclass()
module = imp.new_module(name)
loader.init_module_attrs(module)
self.assertIs(module.__loader__, loader)
self.assertEqual(module.__file__, path)
self.assertEqual(module.__package__, '')
self.assertFalse(hasattr(module, '__path__'))
def test_package(self):
# Verify __loader__, __file__, __package__, and __path__.
name = 'pkg'
path = os.path.join('some', 'pkg', '__init__.py')
with self.mock_methods(True, path):
loader = ExecutionLoaderSubclass()
module = imp.new_module(name)
loader.init_module_attrs(module)
self.assertIs(module.__loader__, loader)
self.assertEqual(module.__file__, path)
self.assertEqual(module.__package__, 'pkg')
self.assertEqual(module.__path__, [os.path.dirname(path)])
def test_submodule(self):
# Verify __package__ and not __path__; test_toplevel() takes care of
# other attributes.
name = 'pkg.submodule'
path = os.path.join('some', 'pkg', 'submodule.py')
with self.mock_methods(False, path):
loader = ExecutionLoaderSubclass()
module = imp.new_module(name)
loader.init_module_attrs(module)
self.assertEqual(module.__package__, 'pkg')
self.assertEqual(module.__file__, path)
self.assertFalse(hasattr(module, '__path__'))
def test_get_filename_ImportError(self):
# If get_filename() raises ImportError, don't set __file__.
name = 'blah'
path = 'blah.py'
with self.mock_methods(False, path) as mocked_methods:
mocked_methods['get_filename'].side_effect = ImportError
loader = ExecutionLoaderSubclass()
module = imp.new_module(name)
loader.init_module_attrs(module)
self.assertFalse(hasattr(module, '__file__'))
##### SourceLoader concrete methods ############################################ ##### SourceLoader concrete methods ############################################
class SourceOnlyLoaderMock(abc.SourceLoader): class SourceOnlyLoaderMock(abc.SourceLoader):
@ -621,6 +782,47 @@ class SourceLoaderGetSourceTests(unittest.TestCase):
self.assertEqual(mock.get_source(name), expect) self.assertEqual(mock.get_source(name), expect)
class SourceLoaderInitModuleAttrTests(unittest.TestCase):
"""Tests for importlib.abc.SourceLoader.init_module_attrs()."""
def test_init_module_attrs(self):
# If __file__ set, __cached__ == imp.cached_from_source(__file__).
name = 'blah'
path = 'blah.py'
loader = SourceOnlyLoaderMock(path)
module = imp.new_module(name)
loader.init_module_attrs(module)
self.assertEqual(module.__loader__, loader)
self.assertEqual(module.__package__, '')
self.assertEqual(module.__file__, path)
self.assertEqual(module.__cached__, imp.cache_from_source(path))
@mock.patch('importlib._bootstrap.cache_from_source')
def test_cache_from_source_NotImplementedError(self, mock_cache_from_source):
# If imp.cache_from_source() raises NotImplementedError don't set
# __cached__.
mock_cache_from_source.side_effect = NotImplementedError
name = 'blah'
path = 'blah.py'
loader = SourceOnlyLoaderMock(path)
module = imp.new_module(name)
loader.init_module_attrs(module)
self.assertEqual(module.__file__, path)
self.assertFalse(hasattr(module, '__cached__'))
def test_no_get_filename(self):
# No __file__, no __cached__.
with mock.patch.object(SourceOnlyLoaderMock, 'get_filename') as mocked:
mocked.side_effect = ImportError
name = 'blah'
loader = SourceOnlyLoaderMock('blah.py')
module = imp.new_module(name)
loader.init_module_attrs(module)
self.assertFalse(hasattr(module, '__file__'))
self.assertFalse(hasattr(module, '__cached__'))
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View file

@ -5,6 +5,7 @@ import sys
from test import support from test import support
import types import types
import unittest import unittest
import warnings
class ModuleToLoadTests(unittest.TestCase): class ModuleToLoadTests(unittest.TestCase):
@ -72,14 +73,27 @@ class ModuleForLoaderTests(unittest.TestCase):
"""Tests for importlib.util.module_for_loader.""" """Tests for importlib.util.module_for_loader."""
@staticmethod
def module_for_loader(func):
with warnings.catch_warnings():
warnings.simplefilter('ignore', PendingDeprecationWarning)
return util.module_for_loader(func)
def test_warning(self):
# Should raise a PendingDeprecationWarning when used.
with warnings.catch_warnings():
warnings.simplefilter('error', PendingDeprecationWarning)
with self.assertRaises(PendingDeprecationWarning):
func = util.module_for_loader(lambda x: x)
def return_module(self, name): def return_module(self, name):
fxn = util.module_for_loader(lambda self, module: module) fxn = self.module_for_loader(lambda self, module: module)
return fxn(self, name) return fxn(self, name)
def raise_exception(self, name): def raise_exception(self, name):
def to_wrap(self, module): def to_wrap(self, module):
raise ImportError raise ImportError
fxn = util.module_for_loader(to_wrap) fxn = self.module_for_loader(to_wrap)
try: try:
fxn(self, name) fxn(self, name)
except ImportError: except ImportError:
@ -100,7 +114,7 @@ class ModuleForLoaderTests(unittest.TestCase):
class FakeLoader: class FakeLoader:
def is_package(self, name): def is_package(self, name):
return True return True
@util.module_for_loader @self.module_for_loader
def load_module(self, module): def load_module(self, module):
return module return module
name = 'a.b.c' name = 'a.b.c'
@ -134,7 +148,7 @@ class ModuleForLoaderTests(unittest.TestCase):
def test_decorator_attrs(self): def test_decorator_attrs(self):
def fxn(self, module): pass def fxn(self, module): pass
wrapped = util.module_for_loader(fxn) wrapped = self.module_for_loader(fxn)
self.assertEqual(wrapped.__name__, fxn.__name__) self.assertEqual(wrapped.__name__, fxn.__name__)
self.assertEqual(wrapped.__qualname__, fxn.__qualname__) self.assertEqual(wrapped.__qualname__, fxn.__qualname__)
@ -160,7 +174,7 @@ class ModuleForLoaderTests(unittest.TestCase):
self._pkg = is_package self._pkg = is_package
def is_package(self, name): def is_package(self, name):
return self._pkg return self._pkg
@util.module_for_loader @self.module_for_loader
def load_module(self, module): def load_module(self, module):
return module return module

View file

@ -97,6 +97,12 @@ Core and Builtins
Library Library
------- -------
- Issue #18089: Implement importlib.abc.InspectLoader.load_module.
- Issue #18088: Introduce importlib.abc.Loader.init_module_attrs for setting
module attributes. Leads to the pending deprecation of
importlib.util.module_for_loader.
- Issue #17403: urllib.parse.robotparser normalizes the urls before adding to - Issue #17403: urllib.parse.robotparser normalizes the urls before adding to
ruleline. This helps in handling certain types invalid urls in a conservative ruleline. This helps in handling certain types invalid urls in a conservative
manner. Patch contributed by Mher Movsisyan. manner. Patch contributed by Mher Movsisyan.
@ -104,7 +110,7 @@ Library
- Issue #18070: Have importlib.util.module_for_loader() set attributes - Issue #18070: Have importlib.util.module_for_loader() set attributes
unconditionally in order to properly support reloading. unconditionally in order to properly support reloading.
- Add importlib.util.module_to_load to return a context manager to provide the - Added importlib.util.module_to_load to return a context manager to provide the
proper module object to load. proper module object to load.
- Issue #18025: Fixed a segfault in io.BufferedIOBase.readinto() when raw - Issue #18025: Fixed a segfault in io.BufferedIOBase.readinto() when raw

File diff suppressed because it is too large Load diff