Skip to content

Commit 02795e2

Browse files
authored
Merge pull request #171 from dmtucker/pytest7
Require Pytest 7+
2 parents 6daa0e1 + 0ac85af commit 02795e2

File tree

4 files changed

+43
-104
lines changed

4 files changed

+43
-104
lines changed

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ dependencies = [
3333
"attrs>=19.0",
3434
"filelock>=3.0",
3535
"mypy>=1.0",
36-
"pytest>=4.6",
36+
"pytest>=7.0",
3737
]
3838

3939
[project.entry-points.pytest11]

src/pytest_mypy.py

+10-44
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Mypy static type checker plugin for Pytest"""
22

33
import json
4-
import os
54
from pathlib import Path
65
from tempfile import NamedTemporaryFile
76
from typing import Dict, List, Optional, TextIO
@@ -10,10 +9,9 @@
109
import attr
1110
from filelock import FileLock # type: ignore
1211
import mypy.api
13-
import pytest # type: ignore
12+
import pytest
1413

1514

16-
PYTEST_MAJOR_VERSION = int(pytest.__version__.partition(".")[0])
1715
mypy_argv = []
1816
nodeid_name = "mypy"
1917
terminal_summary_title = "mypy"
@@ -126,27 +124,9 @@ def pytest_collect_file(file_path, parent):
126124
return None
127125

128126

129-
if PYTEST_MAJOR_VERSION < 7: # pragma: no cover
130-
_pytest_collect_file = pytest_collect_file
131-
132-
def pytest_collect_file(path, parent): # type: ignore
133-
try:
134-
# https://docs.pytest.org/en/7.0.x/deprecations.html#py-path-local-arguments-for-hooks-replaced-with-pathlib-path
135-
return _pytest_collect_file(Path(str(path)), parent)
136-
except TypeError:
137-
# https://docs.pytest.org/en/7.0.x/deprecations.html#fspath-argument-for-node-constructors-replaced-with-pathlib-path
138-
return MypyFile.from_parent(parent=parent, fspath=path)
139-
140-
141127
class MypyFile(pytest.File):
142128
"""A File that Mypy will run on."""
143129

144-
@classmethod
145-
def from_parent(cls, *args, **kwargs):
146-
"""Override from_parent for compatibility."""
147-
# pytest.File.from_parent did not exist before pytest 5.4.
148-
return getattr(super(), "from_parent", cls)(*args, **kwargs)
149-
150130
def collect(self):
151131
"""Create a MypyFileItem for the File."""
152132
yield MypyFileItem.from_parent(parent=self, name=nodeid_name)
@@ -169,19 +149,6 @@ def __init__(self, *args, **kwargs):
169149
super().__init__(*args, **kwargs)
170150
self.add_marker(self.MARKER)
171151

172-
def collect(self):
173-
"""
174-
Partially work around https://github.com/pytest-dev/pytest/issues/8016
175-
for pytest < 6.0 with --looponfail.
176-
"""
177-
yield self
178-
179-
@classmethod
180-
def from_parent(cls, *args, **kwargs):
181-
"""Override from_parent for compatibility."""
182-
# pytest.Item.from_parent did not exist before pytest 5.4.
183-
return getattr(super(), "from_parent", cls)(*args, **kwargs)
184-
185152
def repr_failure(self, excinfo):
186153
"""
187154
Unwrap mypy errors so we get a clean error message without the
@@ -198,7 +165,7 @@ class MypyFileItem(MypyItem):
198165
def runtest(self):
199166
"""Raise an exception if mypy found errors for this item."""
200167
results = MypyResults.from_session(self.session)
201-
abspath = os.path.abspath(str(self.fspath))
168+
abspath = str(self.path.absolute())
202169
errors = results.abspath_errors.get(abspath)
203170
if errors:
204171
if not all(
@@ -211,9 +178,9 @@ def runtest(self):
211178
def reportinfo(self):
212179
"""Produce a heading for the test report."""
213180
return (
214-
self.fspath,
181+
self.path,
215182
None,
216-
self.config.invocation_dir.bestrelpath(self.fspath),
183+
str(self.path.relative_to(self.config.invocation_params.dir)),
217184
)
218185

219186

@@ -258,24 +225,23 @@ def from_mypy(
258225
) -> "MypyResults":
259226
"""Generate results from mypy."""
260227

261-
# This is covered by test_mypy_results_from_mypy_with_opts;
262-
# however, coverage is not recognized on py38-pytest4.6:
263-
if opts is None: # pragma: no cover
228+
if opts is None:
264229
opts = mypy_argv[:]
265230
abspath_errors = {
266231
str(path.absolute()): [] for path in paths
267232
} # type: MypyResults._abspath_errors_type
268233

234+
cwd = Path.cwd()
269235
stdout, stderr, status = mypy.api.run(
270-
opts + [os.path.relpath(key) for key in abspath_errors.keys()]
236+
opts + [str(Path(key).relative_to(cwd)) for key in abspath_errors.keys()]
271237
)
272238

273239
unmatched_lines = []
274240
for line in stdout.split("\n"):
275241
if not line:
276242
continue
277243
path, _, error = line.partition(":")
278-
abspath = os.path.abspath(path)
244+
abspath = str(Path(path).absolute())
279245
try:
280246
abspath_errors[abspath].append(error)
281247
except KeyError:
@@ -305,7 +271,7 @@ def from_session(cls, session) -> "MypyResults":
305271
except FileNotFoundError:
306272
results = cls.from_mypy(
307273
[
308-
Path(item.fspath)
274+
item.path
309275
for item in session.items
310276
if isinstance(item, MypyFileItem)
311277
],
@@ -344,4 +310,4 @@ def pytest_terminal_summary(terminalreporter, config):
344310
terminalreporter.write_line(results.unmatched_stdout, **color)
345311
if results.stderr:
346312
terminalreporter.write_line(results.stderr, yellow=True)
347-
os.remove(config._mypy_results_path)
313+
Path(config._mypy_results_path).unlink()

tests/test_pytest_mypy.py

+20-41
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,13 @@ def pyfunc(x: int) -> int:
5353
)
5454
result = testdir.runpytest_subprocess(*xdist_args)
5555
result.assert_outcomes()
56+
assert result.ret == pytest.ExitCode.NO_TESTS_COLLECTED
5657
result = testdir.runpytest_subprocess("--mypy", *xdist_args)
5758
mypy_file_checks = pyfile_count
5859
mypy_status_check = 1
5960
mypy_checks = mypy_file_checks + mypy_status_check
6061
result.assert_outcomes(passed=mypy_checks)
61-
assert result.ret == 0
62+
assert result.ret == pytest.ExitCode.OK
6263

6364

6465
def test_mypy_pyi(testdir, xdist_args):
@@ -89,7 +90,7 @@ def pyfunc(x: int) -> int: ...
8990
mypy_status_check = 1
9091
mypy_checks = mypy_file_checks + mypy_status_check
9192
result.assert_outcomes(passed=mypy_checks)
92-
assert result.ret == 0
93+
assert result.ret == pytest.ExitCode.OK
9394

9495

9596
def test_mypy_error(testdir, xdist_args):
@@ -103,14 +104,15 @@ def pyfunc(x: int) -> str:
103104
result = testdir.runpytest_subprocess(*xdist_args)
104105
result.assert_outcomes()
105106
assert "_mypy_results_path" not in result.stderr.str()
107+
assert result.ret == pytest.ExitCode.NO_TESTS_COLLECTED
106108
result = testdir.runpytest_subprocess("--mypy", *xdist_args)
107109
mypy_file_checks = 1
108110
mypy_status_check = 1
109111
mypy_checks = mypy_file_checks + mypy_status_check
110112
result.assert_outcomes(failed=mypy_checks)
111113
result.stdout.fnmatch_lines(["2: error: Incompatible return value*"])
112-
assert result.ret != 0
113114
assert "_mypy_results_path" not in result.stderr.str()
115+
assert result.ret == pytest.ExitCode.TESTS_FAILED
114116

115117

116118
def test_mypy_annotation_unchecked(testdir, xdist_args, tmp_path, monkeypatch):
@@ -131,7 +133,7 @@ def pyfunc(x):
131133
outcomes = {"passed": mypy_checks}
132134
result.assert_outcomes(**outcomes)
133135
result.stdout.fnmatch_lines(["*MypyWarning*"])
134-
assert result.ret == 0
136+
assert result.ret == pytest.ExitCode.OK
135137

136138

137139
def test_mypy_ignore_missings_imports(testdir, xdist_args):
@@ -162,10 +164,10 @@ def test_mypy_ignore_missings_imports(testdir, xdist_args):
162164
),
163165
],
164166
)
165-
assert result.ret != 0
167+
assert result.ret == pytest.ExitCode.TESTS_FAILED
166168
result = testdir.runpytest_subprocess("--mypy-ignore-missing-imports", *xdist_args)
167169
result.assert_outcomes(passed=mypy_checks)
168-
assert result.ret == 0
170+
assert result.ret == pytest.ExitCode.OK
169171

170172

171173
def test_mypy_config_file(testdir, xdist_args):
@@ -181,7 +183,7 @@ def pyfunc(x):
181183
mypy_status_check = 1
182184
mypy_checks = mypy_file_checks + mypy_status_check
183185
result.assert_outcomes(passed=mypy_checks)
184-
assert result.ret == 0
186+
assert result.ret == pytest.ExitCode.OK
185187
mypy_config_file = testdir.makeini(
186188
"""
187189
[mypy]
@@ -210,10 +212,10 @@ def test_fails():
210212
mypy_status_check = 1
211213
mypy_checks = mypy_file_checks + mypy_status_check
212214
result.assert_outcomes(failed=test_count, passed=mypy_checks)
213-
assert result.ret != 0
215+
assert result.ret == pytest.ExitCode.TESTS_FAILED
214216
result = testdir.runpytest_subprocess("--mypy", "-m", "mypy", *xdist_args)
215217
result.assert_outcomes(passed=mypy_checks)
216-
assert result.ret == 0
218+
assert result.ret == pytest.ExitCode.OK
217219

218220

219221
def test_non_mypy_error(testdir, xdist_args):
@@ -235,6 +237,7 @@ def runtest(self):
235237
)
236238
result = testdir.runpytest_subprocess(*xdist_args)
237239
result.assert_outcomes()
240+
assert result.ret == pytest.ExitCode.NO_TESTS_COLLECTED
238241
result = testdir.runpytest_subprocess("--mypy", *xdist_args)
239242
mypy_file_checks = 1 # conftest.py
240243
mypy_status_check = 1
@@ -243,7 +246,7 @@ def runtest(self):
243246
passed=mypy_status_check, # conftest.py has no type errors.
244247
)
245248
result.stdout.fnmatch_lines(["*" + message])
246-
assert result.ret != 0
249+
assert result.ret == pytest.ExitCode.TESTS_FAILED
247250

248251

249252
def test_mypy_stderr(testdir, xdist_args):
@@ -294,7 +297,7 @@ def pytest_configure(config):
294297
""",
295298
)
296299
result = testdir.runpytest_subprocess("--mypy", *xdist_args)
297-
assert result.ret == 0
300+
assert result.ret == pytest.ExitCode.OK
298301

299302

300303
def test_api_nodeid_name(testdir, xdist_args):
@@ -311,7 +314,7 @@ def pytest_configure(config):
311314
)
312315
result = testdir.runpytest_subprocess("--mypy", "--verbose", *xdist_args)
313316
result.stdout.fnmatch_lines(["*conftest.py::" + nodeid_name + "*"])
314-
assert result.ret == 0
317+
assert result.ret == pytest.ExitCode.OK
315318

316319

317320
@pytest.mark.xfail(
@@ -352,7 +355,7 @@ def pyfunc(x: int) -> str:
352355
mypy_file_checks = 1
353356
mypy_status_check = 1
354357
result.assert_outcomes(passed=mypy_file_checks, failed=mypy_status_check)
355-
assert result.ret != 0
358+
assert result.ret == pytest.ExitCode.TESTS_FAILED
356359

357360

358361
def test_api_error_formatter(testdir, xdist_args):
@@ -381,7 +384,7 @@ def pytest_configure(config):
381384
)
382385
result = testdir.runpytest_subprocess("--mypy", *xdist_args)
383386
result.stdout.fnmatch_lines(["*/bad.py:2: error: Incompatible return value*"])
384-
assert result.ret != 0
387+
assert result.ret == pytest.ExitCode.TESTS_FAILED
385388

386389

387390
def test_pyproject_toml(testdir, xdist_args):
@@ -401,7 +404,7 @@ def pyfunc(x):
401404
)
402405
result = testdir.runpytest_subprocess("--mypy", *xdist_args)
403406
result.stdout.fnmatch_lines(["1: error: Function is missing a type annotation*"])
404-
assert result.ret != 0
407+
assert result.ret == pytest.ExitCode.TESTS_FAILED
405408

406409

407410
def test_setup_cfg(testdir, xdist_args):
@@ -421,7 +424,7 @@ def pyfunc(x):
421424
)
422425
result = testdir.runpytest_subprocess("--mypy", *xdist_args)
423426
result.stdout.fnmatch_lines(["1: error: Function is missing a type annotation*"])
424-
assert result.ret != 0
427+
assert result.ret == pytest.ExitCode.TESTS_FAILED
425428

426429

427430
@pytest.mark.parametrize("module_name", ["__init__", "test_demo"])
@@ -537,30 +540,6 @@ def _break():
537540
child.kill(signal.SIGTERM)
538541

539542

540-
def test_mypy_item_collect(testdir, xdist_args):
541-
"""Ensure coverage for a 3.10<=pytest<6.0 workaround."""
542-
testdir.makepyfile(
543-
"""
544-
def test_mypy_item_collect(request):
545-
plugin = request.config.pluginmanager.getplugin("mypy")
546-
mypy_items = [
547-
item
548-
for item in request.session.items
549-
if isinstance(item, plugin.MypyItem)
550-
]
551-
assert mypy_items
552-
for mypy_item in mypy_items:
553-
assert all(item is mypy_item for item in mypy_item.collect())
554-
""",
555-
)
556-
result = testdir.runpytest_subprocess("--mypy", *xdist_args)
557-
test_count = 1
558-
mypy_file_checks = 1
559-
mypy_status_check = 1
560-
result.assert_outcomes(passed=test_count + mypy_file_checks + mypy_status_check)
561-
assert result.ret == 0
562-
563-
564543
def test_mypy_results_from_mypy_with_opts():
565544
"""MypyResults.from_mypy respects passed options."""
566545
mypy_results = pytest_mypy.MypyResults.from_mypy([], opts=["--version"])
@@ -610,5 +589,5 @@ def pytest_terminal_summary(config):
610589
mypy_status_check = 1
611590
mypy_checks = mypy_file_checks + mypy_status_check
612591
result.assert_outcomes(passed=mypy_checks)
613-
assert result.ret == 0
592+
assert result.ret == pytest.ExitCode.OK
614593
assert f"= {pytest_mypy.terminal_summary_title} =" not in str(result.stdout)

tox.ini

+12-18
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,27 @@
33
minversion = 4.4
44
isolated_build = true
55
envlist =
6-
py37-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{1.0, 1.x}
7-
py38-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}
8-
py39-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}
9-
py310-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}
10-
py311-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}
11-
py312-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}
6+
py37-pytest{7.0, 7.x}-mypy{1.0, 1.x}
7+
py38-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}
8+
py39-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}
9+
py310-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}
10+
py311-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}
11+
py312-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}
1212
publish
1313
static
1414

1515
[gh-actions]
1616
python =
17-
3.7: py37-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x}-mypy{1.0, 1.x}
18-
3.8: py38-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}, publish, static
19-
3.9: py39-pytest{4.6, 5.0, 5.x, 6.0, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}
20-
3.10: py310-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}
21-
3.11: py311-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}
22-
3.12: py312-pytest{6.2, 6.x, 7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}
17+
3.7: py37-pytest{7.0, 7.x}-mypy{1.0, 1.x}
18+
3.8: py38-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}, publish, static
19+
3.9: py39-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}
20+
3.10: py310-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}
21+
3.11: py311-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}
22+
3.12: py312-pytest{7.0, 7.x, 8.0, 8.x}-mypy{1.0, 1.x}
2323

2424
[testenv]
2525
constrain_package_deps = true
2626
deps =
27-
pytest4.6: pytest ~= 4.6.0
28-
pytest5.0: pytest ~= 5.0.0
29-
pytest5.x: pytest ~= 5.0
30-
pytest6.0: pytest ~= 6.0.0
31-
pytest6.2: pytest ~= 6.2.0
32-
pytest6.x: pytest ~= 6.0
3327
pytest7.0: pytest ~= 7.0.0
3428
pytest7.x: pytest ~= 7.0
3529
pytest8.0: pytest ~= 8.0.0

0 commit comments

Comments
 (0)