Kaydet (Commit) 02d84540 authored tarafından Brett Cannon's avatar Brett Cannon

Issue #23014: Make importlib.abc.Loader.create_module() required when

importlib.abc.Loader.exec_module() is also defined.

Before this change, create_module() was optional **and** could return
None to trigger default semantics. This change now reduces the
options for choosing default semantics to one and in the most
backporting-friendly way (define create_module() to return None).
üst 863c69cf
...@@ -347,13 +347,16 @@ ABC hierarchy:: ...@@ -347,13 +347,16 @@ ABC hierarchy::
.. method:: create_module(spec) .. method:: create_module(spec)
An optional method that returns the module object to use when A method that returns the module object to use when
importing a module. create_module() may also return ``None``, importing a module. This method may return ``None``,
indicating that the default module creation should take place indicating that default module creation semantics should take place.
instead.
.. versionadded:: 3.4 .. versionadded:: 3.4
.. versionchanged:: 3.5
Starting in Python 3.6, this method will not be optional when
:meth:`exec_module` is defined.
.. method:: exec_module(module) .. method:: exec_module(module)
An abstract method that executes the module in its own namespace An abstract method that executes the module in its own namespace
...@@ -417,7 +420,7 @@ ABC hierarchy:: ...@@ -417,7 +420,7 @@ ABC hierarchy::
.. deprecated:: 3.4 .. deprecated:: 3.4
The recommended API for loading a module is :meth:`exec_module` The recommended API for loading a module is :meth:`exec_module`
(and optionally :meth:`create_module`). Loaders should implement (and :meth:`create_module`). Loaders should implement
it instead of load_module(). The import machinery takes care of it instead of load_module(). The import machinery takes care of
all the other responsibilities of load_module() when exec_module() all the other responsibilities of load_module() when exec_module()
is implemented. is implemented.
...@@ -1136,9 +1139,9 @@ an :term:`importer`. ...@@ -1136,9 +1139,9 @@ an :term:`importer`.
.. function:: module_from_spec(spec) .. function:: module_from_spec(spec)
Create a new module based on **spec**. Create a new module based on **spec** and ``spec.loader.create_module()``.
If the module object is from ``spec.loader.create_module()``, then any If ``spec.loader.create_module()`` does not return ``None``, then any
pre-existing attributes will not be reset. Also, no :exc:`AttributeError` pre-existing attributes will not be reset. Also, no :exc:`AttributeError`
will be raised if triggered while accessing **spec** or setting an attribute will be raised if triggered while accessing **spec** or setting an attribute
on the module. on the module.
...@@ -1234,9 +1237,10 @@ an :term:`importer`. ...@@ -1234,9 +1237,10 @@ an :term:`importer`.
module has an attribute accessed. module has an attribute accessed.
This class **only** works with loaders that define This class **only** works with loaders that define
:meth:`importlib.abc.Loader.exec_module` as control over what module type :meth:`~importlib.abc.Loader.exec_module` as control over what module type
is used for the module is required. For the same reasons, the loader is used for the module is required. For those same reasons, the loader's
**cannot** define :meth:`importlib.abc.Loader.create_module`. Finally, :meth:`~importlib.abc.Loader.create_module` method will be ignored (i.e., the
loader's method should only return ``None``). Finally,
modules which substitute the object placed into :attr:`sys.modules` will modules which substitute the object placed into :attr:`sys.modules` will
not work as there is no way to properly replace the module references not work as there is no way to properly replace the module references
throughout the interpreter safely; :exc:`ValueError` is raised if such a throughout the interpreter safely; :exc:`ValueError` is raised if such a
......
...@@ -339,6 +339,7 @@ of what happens during the loading portion of import:: ...@@ -339,6 +339,7 @@ of what happens during the loading portion of import::
module = None module = None
if spec.loader is not None and hasattr(spec.loader, 'create_module'): if spec.loader is not None and hasattr(spec.loader, 'create_module'):
# It is assumed 'exec_module' will also be defined on the loader.
module = spec.loader.create_module(spec) module = spec.loader.create_module(spec)
if module is None: if module is None:
module = ModuleType(spec.name) module = ModuleType(spec.name)
...@@ -427,7 +428,7 @@ Module loaders may opt in to creating the module object during loading ...@@ -427,7 +428,7 @@ Module loaders may opt in to creating the module object during loading
by implementing a :meth:`~importlib.abc.Loader.create_module` method. by implementing a :meth:`~importlib.abc.Loader.create_module` method.
It takes one argument, the module spec, and returns the new module object It takes one argument, the module spec, and returns the new module object
to use during loading. ``create_module()`` does not need to set any attributes to use during loading. ``create_module()`` does not need to set any attributes
on the module object. If the loader does not define ``create_module()``, the on the module object. If the method returns ``None``, the
import machinery will create the new module itself. import machinery will create the new module itself.
.. versionadded:: 3.4 .. versionadded:: 3.4
...@@ -462,6 +463,11 @@ import machinery will create the new module itself. ...@@ -462,6 +463,11 @@ import machinery will create the new module itself.
module(s), and only if the loader itself has loaded the module(s) module(s), and only if the loader itself has loaded the module(s)
explicitly. explicitly.
.. versionchanged:: 3.5
A :exc:`DeprecationWarning` is raised when ``exec_module()`` is defined but
``create_module()`` is not. Starting in Python 3.6 it will be an error to not
define ``create_module()`` on a loader attached to a ModuleSpec.
Module spec Module spec
----------- -----------
......
...@@ -456,6 +456,14 @@ Changes in the Python API ...@@ -456,6 +456,14 @@ Changes in the Python API
`http.client` and `http.server` remain available for backwards compatibility. `http.client` and `http.server` remain available for backwards compatibility.
(Contributed by Demian Brecht in :issue:`21793`.) (Contributed by Demian Brecht in :issue:`21793`.)
* When an import loader defines :meth:`~importlib.machinery.Loader.exec_module`
it is now expected to also define
:meth:`~importlib.machinery.Loader.create_module` (raises a
:exc:`DeprecationWarning` now, will be an error in Python 3.6). If the loader
inherits from :class:`importlib.abc.Loader` then there is nothing to do, else
simply define :meth:`~importlib.machinery.Loader.create_module` to return
``None`` (:issue:`23014`).
Changes in the C API Changes in the C API
-------------------- --------------------
......
...@@ -1055,6 +1055,10 @@ def module_from_spec(spec): ...@@ -1055,6 +1055,10 @@ def module_from_spec(spec):
# If create_module() returns `None` then it means default # If create_module() returns `None` then it means default
# module creation should be used. # module creation should be used.
module = spec.loader.create_module(spec) module = spec.loader.create_module(spec)
elif hasattr(spec.loader, 'exec_module'):
_warnings.warn('starting in Python 3.6, loaders defining exec_module() '
'must also define create_module()',
DeprecationWarning, stacklevel=2)
if module is None: if module is None:
module = _new_module(spec.name) module = _new_module(spec.name)
_init_module_attrs(spec, module) _init_module_attrs(spec, module)
...@@ -1298,6 +1302,10 @@ class FrozenImporter: ...@@ -1298,6 +1302,10 @@ class FrozenImporter:
""" """
return cls if _imp.is_frozen(fullname) else None return cls if _imp.is_frozen(fullname) else None
@classmethod
def create_module(cls, spec):
"""Use default semantics for module creation."""
@staticmethod @staticmethod
def exec_module(module): def exec_module(module):
name = module.__spec__.name name = module.__spec__.name
...@@ -1411,6 +1419,9 @@ class _LoaderBasics: ...@@ -1411,6 +1419,9 @@ 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__'
def create_module(self, spec):
"""Use default semantics for module creation."""
def exec_module(self, module): def exec_module(self, module):
"""Execute the module.""" """Execute the module."""
code = self.get_code(module.__name__) code = self.get_code(module.__name__)
...@@ -1771,6 +1782,9 @@ class _NamespaceLoader: ...@@ -1771,6 +1782,9 @@ class _NamespaceLoader:
def get_code(self, fullname): def get_code(self, fullname):
return compile('', '<string>', 'exec', dont_inherit=True) return compile('', '<string>', 'exec', dont_inherit=True)
def create_module(self, spec):
"""Use default semantics for module creation."""
def exec_module(self, module): def exec_module(self, module):
pass pass
......
...@@ -122,9 +122,6 @@ class Loader(metaclass=abc.ABCMeta): ...@@ -122,9 +122,6 @@ class Loader(metaclass=abc.ABCMeta):
This method should raise ImportError if anything prevents it This method should raise ImportError if anything prevents it
from creating a new module. It may return None to indicate from creating a new module. It may return None to indicate
that the spec should create the new module. that the spec should create the new module.
create_module() is optional.
""" """
# By default, defer to default semantics for the new module. # By default, defer to default semantics for the new module.
return None return None
......
...@@ -11,6 +11,9 @@ class SpecLoaderMock: ...@@ -11,6 +11,9 @@ class SpecLoaderMock:
def find_spec(self, fullname, path=None, target=None): def find_spec(self, fullname, path=None, target=None):
return machinery.ModuleSpec(fullname, self) return machinery.ModuleSpec(fullname, self)
def create_module(self, spec):
return None
def exec_module(self, module): def exec_module(self, module):
pass pass
......
...@@ -16,6 +16,10 @@ class BadSpecFinderLoader: ...@@ -16,6 +16,10 @@ class BadSpecFinderLoader:
spec = machinery.ModuleSpec(fullname, cls) spec = machinery.ModuleSpec(fullname, cls)
return spec return spec
@staticmethod
def create_module(spec):
return None
@staticmethod @staticmethod
def exec_module(module): def exec_module(module):
if module.__name__ == SUBMOD_NAME: if module.__name__ == SUBMOD_NAME:
......
...@@ -34,6 +34,9 @@ class TestLoader: ...@@ -34,6 +34,9 @@ class TestLoader:
def _is_package(self, name): def _is_package(self, name):
return self.package return self.package
def create_module(self, spec):
return None
class NewLoader(TestLoader): class NewLoader(TestLoader):
......
...@@ -41,10 +41,16 @@ class DecodeSourceBytesTests: ...@@ -41,10 +41,16 @@ class DecodeSourceBytesTests:
class ModuleFromSpecTests: class ModuleFromSpecTests:
def test_no_create_module(self): def test_no_create_module(self):
class Loader(self.abc.Loader): class Loader:
pass def exec_module(self, module):
pass
spec = self.machinery.ModuleSpec('test', Loader()) spec = self.machinery.ModuleSpec('test', Loader())
module = self.util.module_from_spec(spec) with warnings.catch_warnings(record=True) as w:
warnings.simplefilter('always')
module = self.util.module_from_spec(spec)
self.assertEqual(1, len(w))
self.assertTrue(issubclass(w[0].category, DeprecationWarning))
self.assertIn('create_module', str(w[0].message))
self.assertIsInstance(module, types.ModuleType) self.assertIsInstance(module, types.ModuleType)
self.assertEqual(module.__name__, spec.name) self.assertEqual(module.__name__, spec.name)
......
...@@ -104,6 +104,9 @@ class PkgutilTests(unittest.TestCase): ...@@ -104,6 +104,9 @@ class PkgutilTests(unittest.TestCase):
class PkgutilPEP302Tests(unittest.TestCase): class PkgutilPEP302Tests(unittest.TestCase):
class MyTestLoader(object): class MyTestLoader(object):
def create_module(self, spec):
return None
def exec_module(self, mod): def exec_module(self, mod):
# Count how many times the module is reloaded # Count how many times the module is reloaded
mod.__dict__['loads'] = mod.__dict__.get('loads', 0) + 1 mod.__dict__['loads'] = mod.__dict__.get('loads', 0) + 1
......
This source diff could not be displayed because it is too large. You can view the blob instead.
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