Skip to content

Commit e661890

Browse files
stubtest: add error summary, other output nits (#12855)
Co-authored-by: KotlinIsland <[email protected]> Co-authored-by: Shantanu <[email protected]> Co-authored-by: hauntsaninja <>
1 parent 07edc92 commit e661890

File tree

4 files changed

+137
-69
lines changed

4 files changed

+137
-69
lines changed

mypy/messages.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
)
4343
from mypy.sametypes import is_same_type
4444
from mypy.typeops import separate_union_literals
45-
from mypy.util import unmangle
45+
from mypy.util import unmangle, plural_s
4646
from mypy.errorcodes import ErrorCode
4747
from mypy import message_registry, errorcodes as codes
4848

@@ -2110,14 +2110,6 @@ def strip_quotes(s: str) -> str:
21102110
return s
21112111

21122112

2113-
def plural_s(s: Union[int, Sequence[Any]]) -> str:
2114-
count = s if isinstance(s, int) else len(s)
2115-
if count > 1:
2116-
return 's'
2117-
else:
2118-
return ''
2119-
2120-
21212113
def format_string_list(lst: List[str]) -> str:
21222114
assert len(lst) > 0
21232115
if len(lst) == 1:

mypy/stubtest.py

Lines changed: 84 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
import enum
1010
import importlib
1111
import inspect
12+
import os
1213
import re
1314
import sys
1415
import types
1516
import typing
1617
import typing_extensions
1718
import warnings
19+
from contextlib import redirect_stdout, redirect_stderr
1820
from functools import singledispatch
1921
from pathlib import Path
2022
from typing import Any, Dict, Generic, Iterator, List, Optional, Tuple, TypeVar, Union, cast
@@ -29,7 +31,7 @@
2931
from mypy import nodes
3032
from mypy.config_parser import parse_config_file
3133
from mypy.options import Options
32-
from mypy.util import FancyFormatter, bytes_to_human_readable_repr, is_dunder
34+
from mypy.util import FancyFormatter, bytes_to_human_readable_repr, plural_s, is_dunder
3335

3436

3537
class Missing:
@@ -53,6 +55,10 @@ def _style(message: str, **kwargs: Any) -> str:
5355
return _formatter.style(message, **kwargs)
5456

5557

58+
class StubtestFailure(Exception):
59+
pass
60+
61+
5662
class Error:
5763
def __init__(
5864
self,
@@ -163,19 +169,20 @@ def test_module(module_name: str) -> Iterator[Error]:
163169
"""
164170
stub = get_stub(module_name)
165171
if stub is None:
166-
yield Error([module_name], "failed to find stubs", MISSING, None)
172+
yield Error([module_name], "failed to find stubs", MISSING, None, runtime_desc="N/A")
167173
return
168174

169175
try:
170-
with warnings.catch_warnings():
171-
warnings.simplefilter("ignore")
172-
runtime = importlib.import_module(module_name)
173-
# Also run the equivalent of `from module import *`
174-
# This could have the additional effect of loading not-yet-loaded submodules
175-
# mentioned in __all__
176-
__import__(module_name, fromlist=["*"])
176+
with open(os.devnull, "w") as devnull:
177+
with warnings.catch_warnings(), redirect_stdout(devnull), redirect_stderr(devnull):
178+
warnings.simplefilter("ignore")
179+
runtime = importlib.import_module(module_name)
180+
# Also run the equivalent of `from module import *`
181+
# This could have the additional effect of loading not-yet-loaded submodules
182+
# mentioned in __all__
183+
__import__(module_name, fromlist=["*"])
177184
except Exception as e:
178-
yield Error([module_name], f"failed to import: {e}", stub, MISSING)
185+
yield Error([module_name], f"failed to import, {type(e).__name__}: {e}", stub, MISSING)
179186
return
180187

181188
with warnings.catch_warnings():
@@ -944,7 +951,11 @@ def apply_decorator_to_funcitem(
944951
) or decorator.fullname in mypy.types.OVERLOAD_NAMES:
945952
return func
946953
if decorator.fullname == "builtins.classmethod":
947-
assert func.arguments[0].variable.name in ("cls", "metacls")
954+
if func.arguments[0].variable.name not in ("cls", "mcs", "metacls"):
955+
raise StubtestFailure(
956+
f"unexpected class argument name {func.arguments[0].variable.name!r} "
957+
f"in {dec.fullname}"
958+
)
948959
# FuncItem is written so that copy.copy() actually works, even when compiled
949960
ret = copy.copy(func)
950961
# Remove the cls argument, since it's not present in inspect.signature of classmethods
@@ -1274,26 +1285,16 @@ def build_stubs(modules: List[str], options: Options, find_submodules: bool = Fa
12741285
sources.extend(found_sources)
12751286
all_modules.extend(s.module for s in found_sources if s.module not in all_modules)
12761287

1277-
try:
1278-
res = mypy.build.build(sources=sources, options=options)
1279-
except mypy.errors.CompileError as e:
1280-
output = [
1281-
_style("error: ", color="red", bold=True),
1282-
"not checking stubs due to failed mypy compile:\n",
1283-
str(e),
1284-
]
1285-
print("".join(output))
1286-
raise RuntimeError from e
1287-
if res.errors:
1288-
output = [
1289-
_style("error: ", color="red", bold=True),
1290-
"not checking stubs due to mypy build errors:\n",
1291-
]
1292-
print("".join(output) + "\n".join(res.errors))
1293-
raise RuntimeError
1288+
if sources:
1289+
try:
1290+
res = mypy.build.build(sources=sources, options=options)
1291+
except mypy.errors.CompileError as e:
1292+
raise StubtestFailure(f"failed mypy compile:\n{e}") from e
1293+
if res.errors:
1294+
raise StubtestFailure("mypy build errors:\n" + "\n".join(res.errors))
12941295

1295-
global _all_stubs
1296-
_all_stubs = res.files
1296+
global _all_stubs
1297+
_all_stubs = res.files
12971298

12981299
return all_modules
12991300

@@ -1355,7 +1356,21 @@ def strip_comments(s: str) -> str:
13551356
yield entry
13561357

13571358

1358-
def test_stubs(args: argparse.Namespace, use_builtins_fixtures: bool = False) -> int:
1359+
class _Arguments:
1360+
modules: List[str]
1361+
concise: bool
1362+
ignore_missing_stub: bool
1363+
ignore_positional_only: bool
1364+
allowlist: List[str]
1365+
generate_allowlist: bool
1366+
ignore_unused_allowlist: bool
1367+
mypy_config_file: str
1368+
custom_typeshed_dir: str
1369+
check_typeshed: bool
1370+
version: str
1371+
1372+
1373+
def test_stubs(args: _Arguments, use_builtins_fixtures: bool = False) -> int:
13591374
"""This is stubtest! It's time to test the stubs!"""
13601375
# Load the allowlist. This is a series of strings corresponding to Error.object_desc
13611376
# Values in the dict will store whether we used the allowlist entry or not.
@@ -1371,13 +1386,23 @@ def test_stubs(args: argparse.Namespace, use_builtins_fixtures: bool = False) ->
13711386

13721387
modules = args.modules
13731388
if args.check_typeshed:
1374-
assert not args.modules, "Cannot pass both --check-typeshed and a list of modules"
1389+
if args.modules:
1390+
print(
1391+
_style("error:", color="red", bold=True),
1392+
"cannot pass both --check-typeshed and a list of modules",
1393+
)
1394+
return 1
13751395
modules = get_typeshed_stdlib_modules(args.custom_typeshed_dir)
13761396
# typeshed added a stub for __main__, but that causes stubtest to check itself
13771397
annoying_modules = {"antigravity", "this", "__main__"}
13781398
modules = [m for m in modules if m not in annoying_modules]
13791399

1380-
assert modules, "No modules to check"
1400+
if not modules:
1401+
print(
1402+
_style("error:", color="red", bold=True),
1403+
"no modules to check",
1404+
)
1405+
return 1
13811406

13821407
options = Options()
13831408
options.incremental = False
@@ -1392,10 +1417,15 @@ def set_strict_flags() -> None: # not needed yet
13921417

13931418
try:
13941419
modules = build_stubs(modules, options, find_submodules=not args.check_typeshed)
1395-
except RuntimeError:
1420+
except StubtestFailure as stubtest_failure:
1421+
print(
1422+
_style("error:", color="red", bold=True),
1423+
f"not checking stubs due to {stubtest_failure}",
1424+
)
13961425
return 1
13971426

13981427
exit_code = 0
1428+
error_count = 0
13991429
for module in modules:
14001430
for error in test_module(module):
14011431
# Filter errors
@@ -1421,6 +1451,7 @@ def set_strict_flags() -> None: # not needed yet
14211451
generated_allowlist.add(error.object_desc)
14221452
continue
14231453
print(error.get_description(concise=args.concise))
1454+
error_count += 1
14241455

14251456
# Print unused allowlist entries
14261457
if not args.ignore_unused_allowlist:
@@ -1429,18 +1460,35 @@ def set_strict_flags() -> None: # not needed yet
14291460
# This lets us allowlist errors that don't manifest at all on some systems
14301461
if not allowlist[w] and not allowlist_regexes[w].fullmatch(""):
14311462
exit_code = 1
1463+
error_count += 1
14321464
print(f"note: unused allowlist entry {w}")
14331465

14341466
# Print the generated allowlist
14351467
if args.generate_allowlist:
14361468
for e in sorted(generated_allowlist):
14371469
print(e)
14381470
exit_code = 0
1471+
elif not args.concise:
1472+
if error_count:
1473+
print(
1474+
_style(
1475+
f"Found {error_count} error{plural_s(error_count)}"
1476+
f" (checked {len(modules)} module{plural_s(modules)})",
1477+
color="red", bold=True
1478+
)
1479+
)
1480+
else:
1481+
print(
1482+
_style(
1483+
f"Success: no issues found in {len(modules)} module{plural_s(modules)}",
1484+
color="green", bold=True
1485+
)
1486+
)
14391487

14401488
return exit_code
14411489

14421490

1443-
def parse_options(args: List[str]) -> argparse.Namespace:
1491+
def parse_options(args: List[str]) -> _Arguments:
14441492
parser = argparse.ArgumentParser(
14451493
description="Compares stubs to objects introspected from the runtime."
14461494
)
@@ -1502,7 +1550,7 @@ def parse_options(args: List[str]) -> argparse.Namespace:
15021550
"--version", action="version", version="%(prog)s " + mypy.version.__version__
15031551
)
15041552

1505-
return parser.parse_args(args)
1553+
return parser.parse_args(args, namespace=_Arguments())
15061554

15071555

15081556
def main() -> int:

mypy/test/teststubtest.py

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -809,7 +809,7 @@ def test_missing_no_runtime_all(self) -> Iterator[Case]:
809809
@collect_cases
810810
def test_non_public_1(self) -> Iterator[Case]:
811811
yield Case(
812-
stub="__all__: list[str]", runtime="", error="test_module.__all__"
812+
stub="__all__: list[str]", runtime="", error=f"{TEST_MODULE_NAME}.__all__"
813813
) # dummy case
814814
yield Case(stub="_f: int", runtime="def _f(): ...", error="_f")
815815

@@ -1085,9 +1085,11 @@ def test_output(self) -> None:
10851085
options=[],
10861086
)
10871087
expected = (
1088-
'error: {0}.bad is inconsistent, stub argument "number" differs from runtime '
1089-
'argument "num"\nStub: at line 1\ndef (number: builtins.int, text: builtins.str)\n'
1090-
"Runtime: at line 1 in file {0}.py\ndef (num, text)\n\n".format(TEST_MODULE_NAME)
1088+
f'error: {TEST_MODULE_NAME}.bad is inconsistent, stub argument "number" differs '
1089+
'from runtime argument "num"\n'
1090+
'Stub: at line 1\ndef (number: builtins.int, text: builtins.str)\n'
1091+
f"Runtime: at line 1 in file {TEST_MODULE_NAME}.py\ndef (num, text)\n\n"
1092+
'Found 1 error (checked 1 module)\n'
10911093
)
10921094
assert remove_color_code(output) == expected
10931095

@@ -1106,17 +1108,17 @@ def test_ignore_flags(self) -> None:
11061108
output = run_stubtest(
11071109
stub="", runtime="__all__ = ['f']\ndef f(): pass", options=["--ignore-missing-stub"]
11081110
)
1109-
assert not output
1111+
assert output == 'Success: no issues found in 1 module\n'
11101112

11111113
output = run_stubtest(
11121114
stub="", runtime="def f(): pass", options=["--ignore-missing-stub"]
11131115
)
1114-
assert not output
1116+
assert output == 'Success: no issues found in 1 module\n'
11151117

11161118
output = run_stubtest(
11171119
stub="def f(__a): ...", runtime="def f(a): pass", options=["--ignore-positional-only"]
11181120
)
1119-
assert not output
1121+
assert output == 'Success: no issues found in 1 module\n'
11201122

11211123
def test_allowlist(self) -> None:
11221124
# Can't use this as a context because Windows
@@ -1130,18 +1132,21 @@ def test_allowlist(self) -> None:
11301132
runtime="def bad(asdf, text): pass",
11311133
options=["--allowlist", allowlist.name],
11321134
)
1133-
assert not output
1135+
assert output == 'Success: no issues found in 1 module\n'
11341136

11351137
# test unused entry detection
11361138
output = run_stubtest(stub="", runtime="", options=["--allowlist", allowlist.name])
1137-
assert output == f"note: unused allowlist entry {TEST_MODULE_NAME}.bad\n"
1139+
assert output == (
1140+
f"note: unused allowlist entry {TEST_MODULE_NAME}.bad\n"
1141+
"Found 1 error (checked 1 module)\n"
1142+
)
11381143

11391144
output = run_stubtest(
11401145
stub="",
11411146
runtime="",
11421147
options=["--allowlist", allowlist.name, "--ignore-unused-allowlist"],
11431148
)
1144-
assert not output
1149+
assert output == 'Success: no issues found in 1 module\n'
11451150

11461151
# test regex matching
11471152
with open(allowlist.name, mode="w+") as f:
@@ -1166,8 +1171,9 @@ def also_bad(asdf): pass
11661171
),
11671172
options=["--allowlist", allowlist.name, "--generate-allowlist"],
11681173
)
1169-
assert output == "note: unused allowlist entry unused.*\n{}.also_bad\n".format(
1170-
TEST_MODULE_NAME
1174+
assert output == (
1175+
f"note: unused allowlist entry unused.*\n"
1176+
f"{TEST_MODULE_NAME}.also_bad\n"
11711177
)
11721178
finally:
11731179
os.unlink(allowlist.name)
@@ -1189,7 +1195,11 @@ def test_missing_stubs(self) -> None:
11891195
output = io.StringIO()
11901196
with contextlib.redirect_stdout(output):
11911197
test_stubs(parse_options(["not_a_module"]))
1192-
assert "error: not_a_module failed to find stubs" in remove_color_code(output.getvalue())
1198+
assert remove_color_code(output.getvalue()) == (
1199+
"error: not_a_module failed to find stubs\n"
1200+
"Stub:\nMISSING\nRuntime:\nN/A\n\n"
1201+
"Found 1 error (checked 1 module)\n"
1202+
)
11931203

11941204
def test_get_typeshed_stdlib_modules(self) -> None:
11951205
stdlib = mypy.stubtest.get_typeshed_stdlib_modules(None, (3, 6))
@@ -1223,8 +1233,23 @@ def test_config_file(self) -> None:
12231233
)
12241234
output = run_stubtest(stub=stub, runtime=runtime, options=[])
12251235
assert remove_color_code(output) == (
1226-
"error: test_module.temp variable differs from runtime type Literal[5]\n"
1236+
f"error: {TEST_MODULE_NAME}.temp variable differs from runtime type Literal[5]\n"
12271237
"Stub: at line 2\n_decimal.Decimal\nRuntime:\n5\n\n"
1238+
"Found 1 error (checked 1 module)\n"
12281239
)
12291240
output = run_stubtest(stub=stub, runtime=runtime, options=[], config_file=config_file)
1230-
assert output == ""
1241+
assert output == "Success: no issues found in 1 module\n"
1242+
1243+
def test_no_modules(self) -> None:
1244+
output = io.StringIO()
1245+
with contextlib.redirect_stdout(output):
1246+
test_stubs(parse_options([]))
1247+
assert remove_color_code(output.getvalue()) == "error: no modules to check\n"
1248+
1249+
def test_module_and_typeshed(self) -> None:
1250+
output = io.StringIO()
1251+
with contextlib.redirect_stdout(output):
1252+
test_stubs(parse_options(["--check-typeshed", "some_module"]))
1253+
assert remove_color_code(output.getvalue()) == (
1254+
"error: cannot pass both --check-typeshed and a list of modules\n"
1255+
)

0 commit comments

Comments
 (0)