diff --git a/nox/tasks.py b/nox/tasks.py index 918f3f18..e237e4d3 100644 --- a/nox/tasks.py +++ b/nox/tasks.py @@ -13,10 +13,11 @@ # limitations under the License. import ast -import importlib.machinery +import importlib.util import io import json import os +import sys import types from argparse import Namespace from typing import List, Union @@ -31,6 +32,42 @@ from nox.sessions import Result +def _load_and_exec_nox_module(global_config: Namespace) -> types.ModuleType: + """ + Loads, executes, then returns the global_config nox module. + + Args: + global_config (Namespace): The global config. + + Raises: + IOError: If the nox module cannot be loaded. This + exception is chosen such that it will be caught + by load_nox_module and logged appropriately. + + Returns: + types.ModuleType: The initialised nox module. + """ + spec = importlib.util.spec_from_file_location( + "user_nox_module", global_config.noxfile + ) + if not spec: + raise IOError(f"Could not get module spec from {global_config.noxfile}") + + module = importlib.util.module_from_spec(spec) + if not module: + raise IOError(f"Noxfile {global_config.noxfile} is not a valid python module.") + + sys.modules["user_nox_module"] = module + + loader = spec.loader + if not loader: # pragma: no cover + raise IOError(f"Could not get module loader for {global_config.noxfile}") + # See https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly + # unsure why mypy doesn't like this + loader.exec_module(module) # type: ignore + return module + + def load_nox_module(global_config: Namespace) -> Union[types.ModuleType, int]: """Load the user's noxfile and return the module object for it. @@ -64,9 +101,8 @@ def load_nox_module(global_config: Namespace) -> Union[types.ModuleType, int]: # guess. The original working directory (the directory that Nox was # invoked from) gets stored by the .invoke_from "option" in _options. os.chdir(noxfile_parent_dir) - return importlib.machinery.SourceFileLoader( - "user_nox_module", global_config.noxfile - ).load_module() + + return _load_and_exec_nox_module(global_config) except (VersionCheckFailed, InvalidVersionSpecifier) as error: logger.error(str(error)) diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 6ce4e503..2df4cfd5 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -95,9 +95,7 @@ def test_load_nox_module_IOError(caplog): our_noxfile = Path(__file__).parent.parent.joinpath("noxfile.py") config = _options.options.namespace(noxfile=str(our_noxfile)) - with mock.patch( - "nox.tasks.importlib.machinery.SourceFileLoader.load_module" - ) as mock_load: + with mock.patch("nox.tasks.importlib.util.module_from_spec") as mock_load: mock_load.side_effect = IOError assert tasks.load_nox_module(config) == 2 @@ -112,15 +110,35 @@ def test_load_nox_module_OSError(caplog): our_noxfile = Path(__file__).parent.parent.joinpath("noxfile.py") config = _options.options.namespace(noxfile=str(our_noxfile)) - with mock.patch( - "nox.tasks.importlib.machinery.SourceFileLoader.load_module" - ) as mock_load: + with mock.patch("nox.tasks.importlib.util.module_from_spec") as mock_load: mock_load.side_effect = OSError assert tasks.load_nox_module(config) == 2 assert "Failed to load Noxfile" in caplog.text +def test_load_nox_module_invalid_spec(): + our_noxfile = Path(__file__).parent.parent.joinpath("noxfile.py") + config = _options.options.namespace(noxfile=str(our_noxfile)) + + with mock.patch("nox.tasks.importlib.util.spec_from_file_location") as mock_spec: + mock_spec.return_value = None + + with pytest.raises(IOError): + tasks._load_and_exec_nox_module(config) + + +def test_load_nox_module_invalid_module(): + our_noxfile = Path(__file__).parent.parent.joinpath("noxfile.py") + config = _options.options.namespace(noxfile=str(our_noxfile)) + + with mock.patch("nox.tasks.importlib.util.module_from_spec") as mock_spec: + mock_spec.return_value = None + + with pytest.raises(IOError): + tasks._load_and_exec_nox_module(config) + + @pytest.fixture def reset_needs_version(): """Do not leak ``nox.needs_version`` between tests."""