Skip to content

Commit

Permalink
fix: pretend we didn't import third-party packages we use. #1228
Browse files Browse the repository at this point in the history
tomli couldn't use coverage themselves because we imported it early.
Cleaning sys.modules means their own imports will actually execute after
coverage has started, so their files will be properly measured.
  • Loading branch information
nedbat committed Oct 6, 2021
1 parent 6211680 commit 613446c
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 7 deletions.
5 changes: 4 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
27 changes: 27 additions & 0 deletions coverage/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import errno
import hashlib
import importlib
import importlib.util
import inspect
import locale
Expand Down Expand Up @@ -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):
Expand Down
7 changes: 2 additions & 5 deletions coverage/tomlconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
20 changes: 19 additions & 1 deletion tests/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

0 comments on commit 613446c

Please sign in to comment.