Skip to content

Commit 613446c

Browse files
committed
fix: pretend we didn't import third-party packages we use. #1228
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.
1 parent 6211680 commit 613446c

File tree

4 files changed

+52
-7
lines changed

4 files changed

+52
-7
lines changed

CHANGES.rst

+4-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ This list is detailed and covers changes in each pre-release version.
2222
Unreleased
2323
----------
2424

25-
Nothing yet.
25+
- Changed an internal detail of how tomli is imported, so that tomli can use
26+
coverage.py for their own test suite (`issue 1228`_).
27+
28+
.. _issue 1228: https://github.com/nedbat/coveragepy/issues/1228
2629

2730

2831
.. _changes_60:

coverage/misc.py

+27
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import errno
77
import hashlib
8+
import importlib
89
import importlib.util
910
import inspect
1011
import locale
@@ -43,6 +44,32 @@ def isolate_module(mod):
4344
os = isolate_module(os)
4445

4546

47+
def import_third_party(modname):
48+
"""Import a third-party module we need, but might not be installed.
49+
50+
This also cleans out the module after the import, so that coverage won't
51+
appear to have imported it. This lets the third party use coverage for
52+
their own tests.
53+
54+
Arguments:
55+
modname (str): the name of the module to import.
56+
57+
Returns:
58+
The imported module, or None if the module couldn't be imported.
59+
60+
"""
61+
try:
62+
mod = importlib.import_module(modname)
63+
except ImportError:
64+
mod = None
65+
66+
imported = [m for m in sys.modules if m.startswith(modname)]
67+
for name in imported:
68+
del sys.modules[name]
69+
70+
return mod
71+
72+
4673
def dummy_decorator_with_args(*args_unused, **kwargs_unused):
4774
"""Dummy no-op implementation of a decorator with arguments."""
4875
def _decorator(func):

coverage/tomlconfig.py

+2-5
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,10 @@
88
import re
99

1010
from coverage.exceptions import CoverageException
11-
from coverage.misc import substitute_variables
11+
from coverage.misc import import_third_party, substitute_variables
1212

1313
# TOML support is an install-time extra option.
14-
try:
15-
import tomli
16-
except ImportError: # pragma: not covered
17-
tomli = None
14+
tomli = import_third_party("tomli")
1815

1916

2017
class TomlDecodeError(Exception):

tests/test_misc.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33

44
"""Tests of miscellaneous stuff."""
55

6+
import sys
7+
68
import pytest
79

810
from coverage.exceptions import CoverageException
911
from coverage.misc import contract, dummy_decorator_with_args, file_be_gone
10-
from coverage.misc import Hasher, one_of, substitute_variables
12+
from coverage.misc import Hasher, one_of, substitute_variables, import_third_party
1113
from coverage.misc import USE_CONTRACTS
1214

1315
from tests.coveragetest import CoverageTest
@@ -155,3 +157,19 @@ def test_substitute_variables_errors(text):
155157
substitute_variables(text, VARS)
156158
assert text in str(exc_info.value)
157159
assert "Variable NOTHING is undefined" in str(exc_info.value)
160+
161+
162+
class ImportThirdPartyTest(CoverageTest):
163+
"""Test import_third_party."""
164+
165+
run_in_temp_dir = False
166+
167+
def test_success(self):
168+
mod = import_third_party("pytest")
169+
assert mod.__name__ == "pytest"
170+
assert "pytest" not in sys.modules
171+
172+
def test_failure(self):
173+
mod = import_third_party("xyzzy")
174+
assert mod is None
175+
assert "xyzzy" not in sys.modules

0 commit comments

Comments
 (0)