Skip to content

Commit

Permalink
refactor: a better way to have maybe-importable third-party modules
Browse files Browse the repository at this point in the history
  • Loading branch information
nedbat committed Jan 3, 2023
1 parent 98301ed commit 4f3ccf2
Show file tree
Hide file tree
Showing 6 changed files with 18 additions and 47 deletions.
8 changes: 5 additions & 3 deletions coverage/misc.py
Expand Up @@ -83,14 +83,16 @@ def import_third_party(modname):
modname (str): the name of the module to import.
Returns:
The imported module, or None if the module couldn't be imported.
The imported module, and a boolean indicating if the module could be imported.
If the boolean is False, the module returned is not the one you want: don't use it.
"""
with sys_modules_saved():
try:
return importlib.import_module(modname)
return importlib.import_module(modname), True
except ImportError:
return None
return sys, False


def nice_pair(pair):
Expand Down
6 changes: 3 additions & 3 deletions coverage/tomlconfig.py
Expand Up @@ -5,7 +5,6 @@

import os
import re
import sys

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

Expand All @@ -17,9 +16,10 @@

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


class TomlDecodeError(Exception):
Expand Down Expand Up @@ -51,7 +51,7 @@ def read(self, filenames: Iterable[str]) -> List[str]:
toml_text = fp.read()
except OSError:
return []
if sys.version_info >= (3, 11) or tomllib is not None:
if has_tomllib:
try:
self.data = tomllib.loads(toml_text)
except tomllib.TOMLDecodeError as err:
Expand Down
20 changes: 0 additions & 20 deletions tests/helpers.py
Expand Up @@ -15,12 +15,10 @@
import textwrap
import warnings

from types import ModuleType
from typing import (
cast,
Any, Callable, Generator, Iterable, List, Optional, Set, Tuple, Type, Union,
)
from unittest import mock

import pytest

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


def without_module(using_module: ModuleType, missing_module_name: str) -> mock._patch[Any]:
"""
Hide a module for testing.
Use this in a test function to make an optional module unavailable during
the test::
with without_module(product.something, 'tomli'):
use_toml_somehow()
Arguments:
using_module: a module in which to hide `missing_module_name`.
missing_module_name (str): the name of the module to hide.
"""
return mock.patch.object(using_module, missing_module_name, None)


def assert_count_equal(a: Iterable[Union[int, str]], b: Iterable[Union[int, str]]) -> None:
"""
A pytest-friendly implementation of assertCountEqual.
Expand Down
11 changes: 5 additions & 6 deletions tests/test_config.py
Expand Up @@ -15,7 +15,6 @@
from coverage.tomlconfig import TomlConfigParser

from tests.coveragetest import CoverageTest, UsingModulesMixin
from tests.helpers import without_module


class ConfigTest(CoverageTest):
Expand Down Expand Up @@ -713,7 +712,7 @@ def test_nocoveragerc_file_when_specified(self) -> None:

def test_no_toml_installed_no_toml(self) -> None:
# Can't read a toml file that doesn't exist.
with without_module(coverage.tomlconfig, 'tomllib'):
with mock.patch.object(coverage.tomlconfig, "has_tomllib", False):
msg = "Couldn't read 'cov.toml' as a config file"
with pytest.raises(ConfigError, match=msg):
coverage.Coverage(config_file="cov.toml")
Expand All @@ -722,7 +721,7 @@ def test_no_toml_installed_no_toml(self) -> None:
def test_no_toml_installed_explicit_toml(self) -> None:
# Can't specify a toml config file if toml isn't installed.
self.make_file("cov.toml", "# A toml file!")
with without_module(coverage.tomlconfig, 'tomllib'):
with mock.patch.object(coverage.tomlconfig, "has_tomllib", False):
msg = "Can't read 'cov.toml' without TOML support"
with pytest.raises(ConfigError, match=msg):
coverage.Coverage(config_file="cov.toml")
Expand All @@ -735,7 +734,7 @@ def test_no_toml_installed_pyproject_toml(self) -> None:
[tool.coverage.run]
xyzzy = 17
""")
with without_module(coverage.tomlconfig, 'tomllib'):
with mock.patch.object(coverage.tomlconfig, "has_tomllib", False):
msg = "Can't read 'pyproject.toml' without TOML support"
with pytest.raises(ConfigError, match=msg):
coverage.Coverage()
Expand All @@ -748,7 +747,7 @@ def test_no_toml_installed_pyproject_toml_shorter_syntax(self) -> None:
[tool.coverage]
run.parallel = true
""")
with without_module(coverage.tomlconfig, 'tomllib'):
with mock.patch.object(coverage.tomlconfig, "has_tomllib", False):
msg = "Can't read 'pyproject.toml' without TOML support"
with pytest.raises(ConfigError, match=msg):
coverage.Coverage()
Expand All @@ -761,7 +760,7 @@ def test_no_toml_installed_pyproject_no_coverage(self) -> None:
[tool.something]
xyzzy = 17
""")
with without_module(coverage.tomlconfig, 'tomllib'):
with mock.patch.object(coverage.tomlconfig, "has_tomllib", False):
cov = coverage.Coverage()
# We get default settings:
assert not cov.config.timid
Expand Down
7 changes: 4 additions & 3 deletions tests/test_misc.py
Expand Up @@ -118,7 +118,8 @@ def test_success(self):
# Make sure we don't have pytest in sys.modules before we start.
del sys.modules["pytest"]
# Import pytest
mod = import_third_party("pytest")
mod, has = import_third_party("pytest")
assert has
# Yes, it's really pytest:
assert mod.__name__ == "pytest"
print(dir(mod))
Expand All @@ -127,8 +128,8 @@ def test_success(self):
assert "pytest" not in sys.modules

def test_failure(self):
mod = import_third_party("xyzzy")
assert mod is None
_, has = import_third_party("xyzzy")
assert not has
assert "xyzzy" not in sys.modules


Expand Down
13 changes: 1 addition & 12 deletions tests/test_testing.py
Expand Up @@ -12,14 +12,13 @@
import pytest

import coverage
from coverage import tomlconfig
from coverage.exceptions import CoverageWarning
from coverage.files import actual_path

from tests.coveragetest import CoverageTest
from tests.helpers import (
arcs_to_arcz_repr, arcz_to_arcs, assert_count_equal, assert_coverage_warnings,
CheckUniqueFilenames, re_lines, re_lines_text, re_line, without_module,
CheckUniqueFilenames, re_lines, re_lines_text, re_line,
)


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


def test_without_module():
toml1 = tomlconfig.tomllib
with without_module(tomlconfig, 'tomllib'):
toml2 = tomlconfig.tomllib
toml3 = tomlconfig.tomllib

assert toml1 is toml3 is not None
assert toml2 is None


class ArczTest(CoverageTest):
"""Tests of arcz/arcs helpers."""

Expand Down

0 comments on commit 4f3ccf2

Please sign in to comment.