Skip to content

Commit

Permalink
Merge pull request #159 from dmtucker/branch-cov
Browse files Browse the repository at this point in the history
Enable branch code coverage
  • Loading branch information
dmtucker committed Mar 6, 2024
2 parents a266479 + 5c63cb8 commit 930086c
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 25 deletions.
48 changes: 26 additions & 22 deletions src/pytest_mypy.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
PYTEST_MAJOR_VERSION = int(pytest.__version__.partition(".")[0])
mypy_argv = []
nodeid_name = "mypy"
terminal_summary_title = "mypy"


def default_file_error_formatter(item, results, errors):
Expand Down Expand Up @@ -59,10 +60,10 @@ def _get_xdist_workerinput(config_node):
return workerinput


def _is_master(config):
def _is_xdist_controller(config):
"""
True if the code running the given pytest.config object is running in
an xdist master node or not running xdist at all.
an xdist controller node or not running xdist at all.
"""
return _get_xdist_workerinput(config) is None

Expand All @@ -73,7 +74,7 @@ def pytest_configure(config):
register a custom marker for MypyItems,
and configure the plugin based on the CLI.
"""
if _is_master(config):
if _is_xdist_controller(config):

# Get the path to a temporary file and delete it.
# The first MypyItem to run will see the file does not exist,
Expand Down Expand Up @@ -205,8 +206,7 @@ def runtest(self):
for error in errors
):
raise MypyError(file_error_formatter(self, results, errors))
# This line cannot be easily covered on mypy < 0.990:
warnings.warn("\n" + "\n".join(errors), MypyWarning) # pragma: no cover
warnings.warn("\n" + "\n".join(errors), MypyWarning)

def reportinfo(self):
"""Produce a heading for the test report."""
Expand Down Expand Up @@ -258,7 +258,9 @@ def from_mypy(
) -> "MypyResults":
"""Generate results from mypy."""

if opts is None:
# This is covered by test_mypy_results_from_mypy_with_opts;
# however, coverage is not recognized on py38-pytest4.6:
if opts is None: # pragma: no cover
opts = mypy_argv[:]
abspath_errors = {
os.path.abspath(str(item.fspath)): [] for item in items
Expand Down Expand Up @@ -293,7 +295,7 @@ def from_session(cls, session) -> "MypyResults":
"""Load (or generate) cached mypy results for a pytest session."""
results_path = (
session.config._mypy_results_path
if _is_master(session.config)
if _is_xdist_controller(session.config)
else _get_xdist_workerinput(session.config)["_mypy_results_path"]
)
with FileLock(results_path + ".lock"):
Expand Down Expand Up @@ -322,18 +324,20 @@ class MypyWarning(pytest.PytestWarning):

def pytest_terminal_summary(terminalreporter, config):
"""Report stderr and unrecognized lines from stdout."""
if _is_master(config):
try:
with open(config._mypy_results_path, mode="r") as results_f:
results = MypyResults.load(results_f)
except FileNotFoundError:
# No MypyItems executed.
return
if results.unmatched_stdout or results.stderr:
terminalreporter.section("mypy")
if results.unmatched_stdout:
color = {"red": True} if results.status else {"green": True}
terminalreporter.write_line(results.unmatched_stdout, **color)
if results.stderr:
terminalreporter.write_line(results.stderr, yellow=True)
os.remove(config._mypy_results_path)
if not _is_xdist_controller(config):
# This isn't hit in pytest 5.0 for some reason.
return # pragma: no cover
try:
with open(config._mypy_results_path, mode="r") as results_f:
results = MypyResults.load(results_f)
except FileNotFoundError:
# No MypyItems executed.
return
if results.unmatched_stdout or results.stderr:
terminalreporter.section(terminal_summary_title)
if results.unmatched_stdout:
color = {"red": True} if results.status else {"green": True}
terminalreporter.write_line(results.unmatched_stdout, **color)
if results.stderr:
terminalreporter.write_line(results.stderr, yellow=True)
os.remove(config._mypy_results_path)
103 changes: 102 additions & 1 deletion tests/test_pytest_mypy.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
import signal
import sys
import textwrap

import mypy.version
from packaging.version import Version
import pexpect
import pytest

import pytest_mypy


MYPY_VERSION = Version(mypy.version.__version__)
PYTEST_VERSION = Version(pytest.__version__)
PYTHON_VERSION = Version(
".".join(
str(token)
for token in [
sys.version_info.major,
sys.version_info.minor,
sys.version_info.micro,
]
)
)


@pytest.fixture(
Expand Down Expand Up @@ -100,7 +113,7 @@ def pyfunc(x: int) -> str:
assert "_mypy_results_path" not in result.stderr.str()


def test_mypy_annotation_unchecked(testdir, xdist_args):
def test_mypy_annotation_unchecked(testdir, xdist_args, tmp_path, monkeypatch):
"""Verify that annotation-unchecked warnings do not manifest as an error."""
testdir.makepyfile(
"""
Expand All @@ -109,6 +122,29 @@ def pyfunc(x):
return x * y
""",
)
min_mypy_version = Version("0.990")
if MYPY_VERSION < min_mypy_version:
# mypy doesn't emit annotation-unchecked warnings until 0.990:
fake_mypy_path = tmp_path / "mypy"
fake_mypy_path.mkdir()
(fake_mypy_path / "__init__.py").touch()
(fake_mypy_path / "api.py").write_text(
textwrap.dedent(
"""
def run(*args, **kwargs):
return (
"test_mypy_annotation_unchecked.py:2:"
" note: By default the bodies of untyped functions"
" are not checked, consider using --check-untyped-defs"
" [annotation-unchecked]\\nSuccess: no issues found in"
" 1 source file\\n",
"",
0,
)
"""
)
)
monkeypatch.setenv("PYTHONPATH", str(tmp_path))
result = testdir.runpytest_subprocess(*xdist_args)
result.assert_outcomes()
result = testdir.runpytest_subprocess("--mypy", *xdist_args)
Expand Down Expand Up @@ -552,3 +588,68 @@ def test_mypy_item_collect(request):
mypy_status_check = 1
result.assert_outcomes(passed=test_count + mypy_file_checks + mypy_status_check)
assert result.ret == 0


@pytest.mark.xfail(
MYPY_VERSION < Version("0.750"),
raises=AssertionError,
reason="https://github.com/python/mypy/issues/7800",
)
def test_mypy_results_from_mypy_with_opts():
"""MypyResults.from_mypy respects passed options."""
mypy_results = pytest_mypy.MypyResults.from_mypy([], opts=["--version"])
assert mypy_results.status == 0
assert mypy_results.abspath_errors == {}
assert str(MYPY_VERSION) in mypy_results.stdout


@pytest.mark.xfail(
Version("3.7") < PYTHON_VERSION < Version("3.9")
and Version("0.710") <= MYPY_VERSION < Version("0.720"),
raises=AssertionError,
reason="Mypy crashes for some reason.",
)
def test_mypy_no_output(testdir, xdist_args):
"""No terminal summary is shown if there is no output from mypy."""
type_ignore = (
"# type: ignore"
if (
PYTEST_VERSION
< Version("6.0") # Pytest didn't add type annotations until 6.0.
or MYPY_VERSION < Version("0.710")
)
else ""
)
testdir.makepyfile(
# Mypy prints a success message to stderr by default:
# "Success: no issues found in 1 source file"
# Clear stderr and unmatched_stdout to simulate mypy having no output:
conftest=f"""
import pytest {type_ignore}
@pytest.hookimpl(hookwrapper=True)
def pytest_terminal_summary(config):
mypy_results_path = getattr(config, "_mypy_results_path", None)
if not mypy_results_path:
# xdist worker
return
pytest_mypy = config.pluginmanager.getplugin("mypy")
with open(mypy_results_path, mode="w") as results_f:
pytest_mypy.MypyResults(
opts=[],
stdout="",
stderr="",
status=0,
abspath_errors={{}},
unmatched_stdout="",
).dump(results_f)
yield
""",
)
result = testdir.runpytest_subprocess("--mypy", *xdist_args)
mypy_file_checks = 1
mypy_status_check = 1
mypy_checks = mypy_file_checks + mypy_status_check
result.assert_outcomes(passed=mypy_checks)
assert result.ret == 0
assert f"= {pytest_mypy.terminal_summary_title} =" not in str(result.stdout)
4 changes: 2 additions & 2 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ deps =

packaging ~= 21.3
pexpect ~= 4.8.0
pytest-cov ~= 2.10
pytest-cov ~= 4.1.0
pytest-randomly ~= 3.4
pytest-xdist ~= 1.34

commands = pytest -p no:mypy {posargs:--cov pytest_mypy --cov-fail-under 100 --cov-report term-missing -n auto}
commands = pytest -p no:mypy {posargs:--cov pytest_mypy --cov-branch --cov-fail-under 100 --cov-report term-missing -n auto}

[pytest]
testpaths = tests
Expand Down

0 comments on commit 930086c

Please sign in to comment.