Skip to content

Commit 4f3ccf2

Browse files
committed
refactor: a better way to have maybe-importable third-party modules
1 parent 98301ed commit 4f3ccf2

File tree

6 files changed

+18
-47
lines changed

6 files changed

+18
-47
lines changed

coverage/misc.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,16 @@ def import_third_party(modname):
8383
modname (str): the name of the module to import.
8484
8585
Returns:
86-
The imported module, or None if the module couldn't be imported.
86+
The imported module, and a boolean indicating if the module could be imported.
87+
88+
If the boolean is False, the module returned is not the one you want: don't use it.
8789
8890
"""
8991
with sys_modules_saved():
9092
try:
91-
return importlib.import_module(modname)
93+
return importlib.import_module(modname), True
9294
except ImportError:
93-
return None
95+
return sys, False
9496

9597

9698
def nice_pair(pair):

coverage/tomlconfig.py

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

66
import os
77
import re
8-
import sys
98

109
from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type, TypeVar
1110

@@ -17,9 +16,10 @@
1716

1817
if env.PYVERSION >= (3, 11, 0, "alpha", 7):
1918
import tomllib # pylint: disable=import-error
19+
has_tomllib = True
2020
else:
2121
# TOML support on Python 3.10 and below is an install-time extra option.
22-
tomllib = import_third_party("tomli")
22+
tomllib, has_tomllib = import_third_party("tomli")
2323

2424

2525
class TomlDecodeError(Exception):
@@ -51,7 +51,7 @@ def read(self, filenames: Iterable[str]) -> List[str]:
5151
toml_text = fp.read()
5252
except OSError:
5353
return []
54-
if sys.version_info >= (3, 11) or tomllib is not None:
54+
if has_tomllib:
5555
try:
5656
self.data = tomllib.loads(toml_text)
5757
except tomllib.TOMLDecodeError as err:

tests/helpers.py

-20
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,10 @@
1515
import textwrap
1616
import warnings
1717

18-
from types import ModuleType
1918
from typing import (
2019
cast,
2120
Any, Callable, Generator, Iterable, List, Optional, Set, Tuple, Type, Union,
2221
)
23-
from unittest import mock
2422

2523
import pytest
2624

@@ -284,24 +282,6 @@ def change_dir(new_dir: str) -> Generator[None, None, None]:
284282
os.chdir(old_dir)
285283

286284

287-
def without_module(using_module: ModuleType, missing_module_name: str) -> mock._patch[Any]:
288-
"""
289-
Hide a module for testing.
290-
291-
Use this in a test function to make an optional module unavailable during
292-
the test::
293-
294-
with without_module(product.something, 'tomli'):
295-
use_toml_somehow()
296-
297-
Arguments:
298-
using_module: a module in which to hide `missing_module_name`.
299-
missing_module_name (str): the name of the module to hide.
300-
301-
"""
302-
return mock.patch.object(using_module, missing_module_name, None)
303-
304-
305285
def assert_count_equal(a: Iterable[Union[int, str]], b: Iterable[Union[int, str]]) -> None:
306286
"""
307287
A pytest-friendly implementation of assertCountEqual.

tests/test_config.py

+5-6
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
from coverage.tomlconfig import TomlConfigParser
1616

1717
from tests.coveragetest import CoverageTest, UsingModulesMixin
18-
from tests.helpers import without_module
1918

2019

2120
class ConfigTest(CoverageTest):
@@ -713,7 +712,7 @@ def test_nocoveragerc_file_when_specified(self) -> None:
713712

714713
def test_no_toml_installed_no_toml(self) -> None:
715714
# Can't read a toml file that doesn't exist.
716-
with without_module(coverage.tomlconfig, 'tomllib'):
715+
with mock.patch.object(coverage.tomlconfig, "has_tomllib", False):
717716
msg = "Couldn't read 'cov.toml' as a config file"
718717
with pytest.raises(ConfigError, match=msg):
719718
coverage.Coverage(config_file="cov.toml")
@@ -722,7 +721,7 @@ def test_no_toml_installed_no_toml(self) -> None:
722721
def test_no_toml_installed_explicit_toml(self) -> None:
723722
# Can't specify a toml config file if toml isn't installed.
724723
self.make_file("cov.toml", "# A toml file!")
725-
with without_module(coverage.tomlconfig, 'tomllib'):
724+
with mock.patch.object(coverage.tomlconfig, "has_tomllib", False):
726725
msg = "Can't read 'cov.toml' without TOML support"
727726
with pytest.raises(ConfigError, match=msg):
728727
coverage.Coverage(config_file="cov.toml")
@@ -735,7 +734,7 @@ def test_no_toml_installed_pyproject_toml(self) -> None:
735734
[tool.coverage.run]
736735
xyzzy = 17
737736
""")
738-
with without_module(coverage.tomlconfig, 'tomllib'):
737+
with mock.patch.object(coverage.tomlconfig, "has_tomllib", False):
739738
msg = "Can't read 'pyproject.toml' without TOML support"
740739
with pytest.raises(ConfigError, match=msg):
741740
coverage.Coverage()
@@ -748,7 +747,7 @@ def test_no_toml_installed_pyproject_toml_shorter_syntax(self) -> None:
748747
[tool.coverage]
749748
run.parallel = true
750749
""")
751-
with without_module(coverage.tomlconfig, 'tomllib'):
750+
with mock.patch.object(coverage.tomlconfig, "has_tomllib", False):
752751
msg = "Can't read 'pyproject.toml' without TOML support"
753752
with pytest.raises(ConfigError, match=msg):
754753
coverage.Coverage()
@@ -761,7 +760,7 @@ def test_no_toml_installed_pyproject_no_coverage(self) -> None:
761760
[tool.something]
762761
xyzzy = 17
763762
""")
764-
with without_module(coverage.tomlconfig, 'tomllib'):
763+
with mock.patch.object(coverage.tomlconfig, "has_tomllib", False):
765764
cov = coverage.Coverage()
766765
# We get default settings:
767766
assert not cov.config.timid

tests/test_misc.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@ def test_success(self):
118118
# Make sure we don't have pytest in sys.modules before we start.
119119
del sys.modules["pytest"]
120120
# Import pytest
121-
mod = import_third_party("pytest")
121+
mod, has = import_third_party("pytest")
122+
assert has
122123
# Yes, it's really pytest:
123124
assert mod.__name__ == "pytest"
124125
print(dir(mod))
@@ -127,8 +128,8 @@ def test_success(self):
127128
assert "pytest" not in sys.modules
128129

129130
def test_failure(self):
130-
mod = import_third_party("xyzzy")
131-
assert mod is None
131+
_, has = import_third_party("xyzzy")
132+
assert not has
132133
assert "xyzzy" not in sys.modules
133134

134135

tests/test_testing.py

+1-12
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,13 @@
1212
import pytest
1313

1414
import coverage
15-
from coverage import tomlconfig
1615
from coverage.exceptions import CoverageWarning
1716
from coverage.files import actual_path
1817

1918
from tests.coveragetest import CoverageTest
2019
from tests.helpers import (
2120
arcs_to_arcz_repr, arcz_to_arcs, assert_count_equal, assert_coverage_warnings,
22-
CheckUniqueFilenames, re_lines, re_lines_text, re_line, without_module,
21+
CheckUniqueFilenames, re_lines, re_lines_text, re_line,
2322
)
2423

2524

@@ -356,16 +355,6 @@ def _same_python_executable(e1, e2):
356355
return False # pragma: only failure
357356

358357

359-
def test_without_module():
360-
toml1 = tomlconfig.tomllib
361-
with without_module(tomlconfig, 'tomllib'):
362-
toml2 = tomlconfig.tomllib
363-
toml3 = tomlconfig.tomllib
364-
365-
assert toml1 is toml3 is not None
366-
assert toml2 is None
367-
368-
369358
class ArczTest(CoverageTest):
370359
"""Tests of arcz/arcs helpers."""
371360

0 commit comments

Comments
 (0)