diff --git a/CHANGES.rst b/CHANGES.rst index 50a81aaf3..937d2d0fa 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -22,7 +22,10 @@ This list is detailed and covers changes in each pre-release version. Unreleased ---------- -Nothing yet. +- Changed an internal detail of how tomli is imported, so that tomli can use + coverage.py for their own test suite (`issue 1228`_). + +.. _issue 1228: https://github.com/nedbat/coveragepy/issues/1228 .. _changes_60: diff --git a/coverage/misc.py b/coverage/misc.py index 11dad23e0..cd4a77401 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -5,6 +5,7 @@ import errno import hashlib +import importlib import importlib.util import inspect import locale @@ -43,6 +44,32 @@ def isolate_module(mod): os = isolate_module(os) +def import_third_party(modname): + """Import a third-party module we need, but might not be installed. + + This also cleans out the module after the import, so that coverage won't + appear to have imported it. This lets the third party use coverage for + their own tests. + + Arguments: + modname (str): the name of the module to import. + + Returns: + The imported module, or None if the module couldn't be imported. + + """ + try: + mod = importlib.import_module(modname) + except ImportError: + mod = None + + imported = [m for m in sys.modules if m.startswith(modname)] + for name in imported: + del sys.modules[name] + + return mod + + def dummy_decorator_with_args(*args_unused, **kwargs_unused): """Dummy no-op implementation of a decorator with arguments.""" def _decorator(func): diff --git a/coverage/tomlconfig.py b/coverage/tomlconfig.py index 203192c93..3301acc8e 100644 --- a/coverage/tomlconfig.py +++ b/coverage/tomlconfig.py @@ -8,13 +8,10 @@ import re from coverage.exceptions import CoverageException -from coverage.misc import substitute_variables +from coverage.misc import import_third_party, substitute_variables # TOML support is an install-time extra option. -try: - import tomli -except ImportError: # pragma: not covered - tomli = None +tomli = import_third_party("tomli") class TomlDecodeError(Exception): diff --git a/tests/test_misc.py b/tests/test_misc.py index 3858c4f8b..077c24344 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -3,11 +3,13 @@ """Tests of miscellaneous stuff.""" +import sys + import pytest from coverage.exceptions import CoverageException from coverage.misc import contract, dummy_decorator_with_args, file_be_gone -from coverage.misc import Hasher, one_of, substitute_variables +from coverage.misc import Hasher, one_of, substitute_variables, import_third_party from coverage.misc import USE_CONTRACTS from tests.coveragetest import CoverageTest @@ -155,3 +157,19 @@ def test_substitute_variables_errors(text): substitute_variables(text, VARS) assert text in str(exc_info.value) assert "Variable NOTHING is undefined" in str(exc_info.value) + + +class ImportThirdPartyTest(CoverageTest): + """Test import_third_party.""" + + run_in_temp_dir = False + + def test_success(self): + mod = import_third_party("pytest") + assert mod.__name__ == "pytest" + assert "pytest" not in sys.modules + + def test_failure(self): + mod = import_third_party("xyzzy") + assert mod is None + assert "xyzzy" not in sys.modules