Skip to content

Commit 0b66e30

Browse files
committed
Create MypyStatusItem
1 parent 6cd5304 commit 0b66e30

File tree

2 files changed

+106
-8
lines changed

2 files changed

+106
-8
lines changed

src/pytest_mypy.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,24 @@ def pytest_collect_file(path, parent):
9090
return None
9191

9292

93+
@pytest.hookimpl(hookwrapper=True, trylast=True)
94+
def pytest_collection_modifyitems(session, config, items):
95+
"""
96+
Add a MypyStatusItem if any MypyFileItems were collected.
97+
98+
Since mypy might check files that were not collected,
99+
pytest could pass even though mypy failed!
100+
To prevent that, add an explicit check for the mypy exit status.
101+
102+
This should execute as late as possible to avoid missing any
103+
MypyFileItems injected by other pytest_collection_modifyitems
104+
implementations.
105+
"""
106+
yield
107+
if any(isinstance(item, MypyFileItem) for item in items):
108+
items.append(MypyStatusItem(nodeid_name, session, config, session))
109+
110+
93111
class MypyItem(pytest.Item):
94112

95113
"""A Mypy-related test Item."""
@@ -131,6 +149,21 @@ def reportinfo(self):
131149
)
132150

133151

152+
class MypyStatusItem(MypyItem):
153+
154+
"""A check for a non-zero mypy exit status."""
155+
156+
def runtest(self):
157+
"""Raise a MypyError if mypy exited with a non-zero status."""
158+
results = _mypy_results(self.session)
159+
if results['status']:
160+
raise MypyError(
161+
'mypy exited with status {status}.'.format(
162+
status=results['status'],
163+
),
164+
)
165+
166+
134167
def _mypy_results(session):
135168
"""Get the cached mypy results for the session, or generate them."""
136169
return _cached_json_results(

tests/test_pytest_mypy.py

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@ def pyfunc(x: int) -> int:
2929
result = testdir.runpytest_subprocess(*xdist_args)
3030
result.assert_outcomes()
3131
result = testdir.runpytest_subprocess('--mypy', *xdist_args)
32-
result.assert_outcomes(passed=pyfile_count)
32+
mypy_file_checks = pyfile_count
33+
mypy_status_check = 1
34+
mypy_checks = mypy_file_checks + mypy_status_check
35+
result.assert_outcomes(passed=mypy_checks)
3336
assert result.ret == 0
3437

3538

@@ -42,7 +45,10 @@ def pyfunc(x: int) -> str:
4245
result = testdir.runpytest_subprocess(*xdist_args)
4346
result.assert_outcomes()
4447
result = testdir.runpytest_subprocess('--mypy', *xdist_args)
45-
result.assert_outcomes(failed=1)
48+
mypy_file_checks = 1
49+
mypy_status_check = 1
50+
mypy_checks = mypy_file_checks + mypy_status_check
51+
result.assert_outcomes(failed=mypy_checks)
4652
result.stdout.fnmatch_lines([
4753
'2: error: Incompatible return value*',
4854
])
@@ -62,7 +68,10 @@ def test_mypy_ignore_missings_imports(testdir, xdist_args):
6268
pass
6369
'''.format(module_name=module_name))
6470
result = testdir.runpytest_subprocess('--mypy', *xdist_args)
65-
result.assert_outcomes(failed=1)
71+
mypy_file_checks = 1
72+
mypy_status_check = 1
73+
mypy_checks = mypy_file_checks + mypy_status_check
74+
result.assert_outcomes(failed=mypy_checks)
6675
result.stdout.fnmatch_lines([
6776
"2: error: Cannot find *module named '{module_name}'".format(
6877
module_name=module_name,
@@ -73,7 +82,7 @@ def test_mypy_ignore_missings_imports(testdir, xdist_args):
7382
'--mypy-ignore-missing-imports',
7483
*xdist_args
7584
)
76-
result.assert_outcomes(passed=1)
85+
result.assert_outcomes(passed=mypy_checks)
7786
assert result.ret == 0
7887

7988

@@ -84,10 +93,14 @@ def test_fails():
8493
assert False
8594
''')
8695
result = testdir.runpytest_subprocess('--mypy', *xdist_args)
87-
result.assert_outcomes(failed=1, passed=1)
96+
test_count = 1
97+
mypy_file_checks = 1
98+
mypy_status_check = 1
99+
mypy_checks = mypy_file_checks + mypy_status_check
100+
result.assert_outcomes(failed=test_count, passed=mypy_checks)
88101
assert result.ret != 0
89102
result = testdir.runpytest_subprocess('--mypy', '-m', 'mypy', *xdist_args)
90-
result.assert_outcomes(passed=1)
103+
result.assert_outcomes(passed=mypy_checks)
91104
assert result.ret == 0
92105

93106

@@ -107,7 +120,12 @@ def runtest(self):
107120
result = testdir.runpytest_subprocess(*xdist_args)
108121
result.assert_outcomes()
109122
result = testdir.runpytest_subprocess('--mypy', *xdist_args)
110-
result.assert_outcomes(failed=1)
123+
mypy_file_checks = 1 # conftest.py
124+
mypy_status_check = 1
125+
result.assert_outcomes(
126+
failed=mypy_file_checks, # patched to raise an Exception
127+
passed=mypy_status_check, # conftest.py has no type errors.
128+
)
111129
result.stdout.fnmatch_lines(['*' + message])
112130
assert result.ret != 0
113131

@@ -171,6 +189,9 @@ def test_pytest_collection_modifyitems(testdir, xdist_args):
171189
Verify that collected files which are removed in a
172190
pytest_collection_modifyitems implementation are not
173191
checked by mypy.
192+
193+
This would also fail if a MypyStatusItem were injected
194+
despite there being no MypyFileItems.
174195
"""
175196
testdir.makepyfile(conftest='''
176197
def pytest_collection_modifyitems(session, config, items):
@@ -190,5 +211,49 @@ def test_pass():
190211
pass
191212
''')
192213
result = testdir.runpytest_subprocess('--mypy', *xdist_args)
193-
result.assert_outcomes(passed=1)
214+
test_count = 1
215+
result.assert_outcomes(passed=test_count)
194216
assert result.ret == 0
217+
218+
219+
def test_mypy_indirect(testdir, xdist_args):
220+
"""Verify that uncollected files checked by mypy cause a failure."""
221+
testdir.makepyfile(bad='''
222+
def pyfunc(x: int) -> str:
223+
return x * 2
224+
''')
225+
testdir.makepyfile(good='''
226+
import bad
227+
''')
228+
xdist_args.append('good.py') # Nothing may come after xdist_args in py34.
229+
result = testdir.runpytest_subprocess('--mypy', *xdist_args)
230+
assert result.ret != 0
231+
232+
233+
def test_mypy_indirect_inject(testdir, xdist_args):
234+
"""
235+
Verify that uncollected files checked by mypy because of a MypyFileItem
236+
injected in pytest_collection_modifyitems cause a failure.
237+
"""
238+
testdir.makepyfile(bad='''
239+
def pyfunc(x: int) -> str:
240+
return x * 2
241+
''')
242+
testdir.makepyfile(good='''
243+
import bad
244+
''')
245+
testdir.makepyfile(conftest='''
246+
import py
247+
import pytest
248+
249+
@pytest.hookimpl(trylast=True) # Inject as late as possible.
250+
def pytest_collection_modifyitems(session, config, items):
251+
plugin = config.pluginmanager.getplugin('mypy')
252+
items.append(
253+
plugin.MypyFileItem(py.path.local('good.py'), session),
254+
)
255+
''')
256+
testdir.mkdir('empty')
257+
xdist_args.append('empty') # Nothing may come after xdist_args in py34.
258+
result = testdir.runpytest_subprocess('--mypy', *xdist_args)
259+
assert result.ret != 0

0 commit comments

Comments
 (0)