Skip to content

Commit b6e7a41

Browse files
committed
stubtest improvements
- error summary - fix issue where module is saved as stub - handle loading modules that output or raise - output messages instead of tracebacks - add some more tests Also: - move plural_s from messages to util - use plural_s in summary functions
1 parent 6c2690e commit b6e7a41

File tree

4 files changed

+180
-44
lines changed

4 files changed

+180
-44
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 as 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: 110 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,25 @@
99
import enum
1010
import importlib
1111
import inspect
12+
import io
1213
import re
1314
import sys
1415
import types
1516
import warnings
17+
from contextlib import redirect_stdout, redirect_stderr
1618
from functools import singledispatch
1719
from pathlib import Path
18-
from typing import Any, Dict, Generic, Iterator, List, Optional, Tuple, TypeVar, Union, cast
20+
from typing import Any, Dict, Generic, Iterator, List, Optional, Tuple, TypeVar, Union, cast, Set
1921

20-
from typing_extensions import Type
22+
from typing_extensions import Type, Literal
2123

2224
import mypy.build
2325
import mypy.modulefinder
2426
import mypy.state
2527
import mypy.types
2628
from mypy import nodes
2729
from mypy.config_parser import parse_config_file
30+
from mypy.messages import plural_s
2831
from mypy.options import Options
2932
from mypy.util import FancyFormatter, bytes_to_human_readable_repr, is_dunder
3033

@@ -50,6 +53,16 @@ def _style(message: str, **kwargs: Any) -> str:
5053
return _formatter.style(message, **kwargs)
5154

5255

56+
def log_error(message: str) -> Literal[1]:
57+
"""Print a bold red message."""
58+
print(_style(message, color="red", bold=True))
59+
return 1
60+
61+
62+
class Failure(Exception):
63+
"""Used to indicate a handled failure state"""
64+
65+
5366
class Error:
5467
def __init__(
5568
self,
@@ -150,7 +163,7 @@ def get_description(self, concise: bool = False) -> str:
150163
# ====================
151164

152165

153-
def test_module(module_name: str) -> Iterator[Error]:
166+
def test_module(module_name: str, concise: bool = False) -> Iterator[Error]:
154167
"""Tests a given module's stub against introspecting it at runtime.
155168
156169
Requires the stub to have been built already, accomplished by a call to ``build_stubs``.
@@ -160,20 +173,40 @@ def test_module(module_name: str) -> Iterator[Error]:
160173
"""
161174
stub = get_stub(module_name)
162175
if stub is None:
163-
yield Error([module_name], "failed to find stubs", MISSING, None)
176+
yield Error([module_name], "failed to find stubs", MISSING, None, runtime_desc="N/A")
164177
return
165178

179+
argv = sys.argv
180+
sys.argv = []
181+
output = io.StringIO()
182+
outerror = io.StringIO()
166183
try:
167-
with warnings.catch_warnings():
184+
with warnings.catch_warnings(), redirect_stdout(output), redirect_stderr(outerror):
168185
warnings.simplefilter("ignore")
169186
runtime = importlib.import_module(module_name)
170187
# Also run the equivalent of `from module import *`
171188
# This could have the additional effect of loading not-yet-loaded submodules
172189
# mentioned in __all__
173190
__import__(module_name, fromlist=["*"])
174-
except Exception as e:
175-
yield Error([module_name], f"failed to import: {e}", stub, MISSING)
191+
except KeyboardInterrupt:
192+
raise
193+
except BaseException as e: # to catch every possible error
194+
yield Error([module_name], f"failed to import: {type(e).__name__} {e}", stub, MISSING,
195+
stub_desc=stub.path, runtime_desc="Missing due to failed import")
176196
return
197+
finally:
198+
sys.argv = argv
199+
stdout = output.getvalue()
200+
stderr = outerror.getvalue()
201+
if stdout or stderr and not concise:
202+
print(f"Found output while loading '{module_name}'")
203+
if stdout:
204+
print(_style("======= standard output ============", bold=True))
205+
print(stdout, end="" if stdout[-1] == "\n" else "\n")
206+
if stderr:
207+
print(_style("======= standard error =============", bold=True))
208+
print(stderr, end="" if stderr[-1] == "\n" else "\n")
209+
print(_style("====================================", bold=True))
177210

178211
with warnings.catch_warnings():
179212
warnings.simplefilter("ignore")
@@ -458,21 +491,21 @@ def get_name(arg: Any) -> str:
458491
return arg.name
459492
if isinstance(arg, nodes.Argument):
460493
return arg.variable.name
461-
raise AssertionError
494+
raise Failure
462495

463496
def get_type(arg: Any) -> Optional[str]:
464497
if isinstance(arg, inspect.Parameter):
465498
return None
466499
if isinstance(arg, nodes.Argument):
467500
return str(arg.variable.type or arg.type_annotation)
468-
raise AssertionError
501+
raise Failure
469502

470503
def has_default(arg: Any) -> bool:
471504
if isinstance(arg, inspect.Parameter):
472505
return arg.default != inspect.Parameter.empty
473506
if isinstance(arg, nodes.Argument):
474507
return arg.kind.is_optional()
475-
raise AssertionError
508+
raise Failure
476509

477510
def get_desc(arg: Any) -> str:
478511
arg_type = get_type(arg)
@@ -507,7 +540,7 @@ def from_funcitem(stub: nodes.FuncItem) -> "Signature[nodes.Argument]":
507540
elif stub_arg.kind == nodes.ARG_STAR2:
508541
stub_sig.varkw = stub_arg
509542
else:
510-
raise AssertionError
543+
raise Failure
511544
return stub_sig
512545

513546
@staticmethod
@@ -526,7 +559,7 @@ def from_inspect_signature(signature: inspect.Signature) -> "Signature[inspect.P
526559
elif runtime_arg.kind == inspect.Parameter.VAR_KEYWORD:
527560
runtime_sig.varkw = runtime_arg
528561
else:
529-
raise AssertionError
562+
raise Failure
530563
return runtime_sig
531564

532565
@staticmethod
@@ -605,7 +638,7 @@ def get_kind(arg_name: str) -> nodes.ArgKind:
605638
elif arg.kind == nodes.ARG_STAR2:
606639
sig.varkw = arg
607640
else:
608-
raise AssertionError
641+
raise Failure
609642
return sig
610643

611644

@@ -915,7 +948,9 @@ def apply_decorator_to_funcitem(
915948
) or decorator.fullname in mypy.types.OVERLOAD_NAMES:
916949
return func
917950
if decorator.fullname == "builtins.classmethod":
918-
assert func.arguments[0].variable.name in ("cls", "metacls")
951+
if func.arguments[0].variable.name not in ("cls", "mcs", "metacls"):
952+
log_error(f"Error: bad class argument name: {func.arguments[0].variable.name}")
953+
raise Failure
919954
# FuncItem is written so that copy.copy() actually works, even when compiled
920955
ret = copy.copy(func)
921956
# Remove the cls argument, since it's not present in inspect.signature of classmethods
@@ -1153,7 +1188,7 @@ def anytype() -> mypy.types.AnyType:
11531188
elif arg.kind == inspect.Parameter.VAR_KEYWORD:
11541189
arg_kinds.append(nodes.ARG_STAR2)
11551190
else:
1156-
raise AssertionError
1191+
raise Failure
11571192
else:
11581193
arg_types = [anytype(), anytype()]
11591194
arg_kinds = [nodes.ARG_STAR, nodes.ARG_STAR2]
@@ -1254,14 +1289,14 @@ def build_stubs(modules: List[str], options: Options, find_submodules: bool = Fa
12541289
str(e),
12551290
]
12561291
print("".join(output))
1257-
raise RuntimeError from e
1292+
raise Failure from e
12581293
if res.errors:
12591294
output = [
12601295
_style("error: ", color="red", bold=True),
12611296
"not checking stubs due to mypy build errors:\n",
12621297
]
12631298
print("".join(output) + "\n".join(res.errors))
1264-
raise RuntimeError
1299+
raise Failure
12651300

12661301
global _all_stubs
12671302
_all_stubs = res.files
@@ -1271,7 +1306,10 @@ def build_stubs(modules: List[str], options: Options, find_submodules: bool = Fa
12711306

12721307
def get_stub(module: str) -> Optional[nodes.MypyFile]:
12731308
"""Returns a stub object for the given module, if we've built one."""
1274-
return _all_stubs.get(module)
1309+
result = _all_stubs.get(module)
1310+
if result and result.is_stub:
1311+
return result
1312+
return None
12751313

12761314

12771315
def get_typeshed_stdlib_modules(
@@ -1326,7 +1364,21 @@ def strip_comments(s: str) -> str:
13261364
yield entry
13271365

13281366

1329-
def test_stubs(args: argparse.Namespace, use_builtins_fixtures: bool = False) -> int:
1367+
class Arguments:
1368+
modules: List[str]
1369+
concise: bool
1370+
ignore_missing_stub: bool
1371+
ignore_positional_only: bool
1372+
allowlist: List[str]
1373+
generate_allowlist: bool
1374+
ignore_unused_allowlist: bool
1375+
mypy_config_file: str
1376+
custom_typeshed_dir: str
1377+
check_typeshed: bool
1378+
error_summary: bool
1379+
1380+
1381+
def test_stubs(args: Arguments, use_builtins_fixtures: bool = False) -> int:
13301382
"""This is stubtest! It's time to test the stubs!"""
13311383
# Load the allowlist. This is a series of strings corresponding to Error.object_desc
13321384
# Values in the dict will store whether we used the allowlist entry or not.
@@ -1342,13 +1394,15 @@ def test_stubs(args: argparse.Namespace, use_builtins_fixtures: bool = False) ->
13421394

13431395
modules = args.modules
13441396
if args.check_typeshed:
1345-
assert not args.modules, "Cannot pass both --check-typeshed and a list of modules"
1397+
if args.modules:
1398+
return log_error("Cannot pass both --check-typeshed and a list of modules")
13461399
modules = get_typeshed_stdlib_modules(args.custom_typeshed_dir)
13471400
# typeshed added a stub for __main__, but that causes stubtest to check itself
13481401
annoying_modules = {"antigravity", "this", "__main__"}
13491402
modules = [m for m in modules if m not in annoying_modules]
13501403

1351-
assert modules, "No modules to check"
1404+
if not modules:
1405+
return log_error("No modules to check")
13521406

13531407
options = Options()
13541408
options.incremental = False
@@ -1363,12 +1417,14 @@ def set_strict_flags() -> None: # not needed yet
13631417

13641418
try:
13651419
modules = build_stubs(modules, options, find_submodules=not args.check_typeshed)
1366-
except RuntimeError:
1420+
except Failure:
13671421
return 1
13681422

13691423
exit_code = 0
1424+
error_count = 0
1425+
error_modules: Set[str] = set()
13701426
for module in modules:
1371-
for error in test_module(module):
1427+
for error in test_module(module, args.concise):
13721428
# Filter errors
13731429
if args.ignore_missing_stub and error.is_missing_stub():
13741430
continue
@@ -1392,6 +1448,8 @@ def set_strict_flags() -> None: # not needed yet
13921448
generated_allowlist.add(error.object_desc)
13931449
continue
13941450
print(error.get_description(concise=args.concise))
1451+
error_count += 1
1452+
error_modules.add(module)
13951453

13961454
# Print unused allowlist entries
13971455
if not args.ignore_unused_allowlist:
@@ -1408,10 +1466,25 @@ def set_strict_flags() -> None: # not needed yet
14081466
print(e)
14091467
exit_code = 0
14101468

1469+
if args.error_summary:
1470+
if not error_count:
1471+
print(
1472+
_style(
1473+
f"Success: no issues found in {len(modules)} module{plural_s(modules)}",
1474+
color="green", bold=True
1475+
)
1476+
)
1477+
else:
1478+
log_error(
1479+
f"Found {error_count} error{plural_s(error_count)} in {len(error_modules)}"
1480+
f" module{plural_s(error_modules)}"
1481+
f" (checked {len(modules)} module{plural_s(modules)})"
1482+
)
1483+
14111484
return exit_code
14121485

14131486

1414-
def parse_options(args: List[str]) -> argparse.Namespace:
1487+
def parse_options(args: List[str]) -> Arguments:
14151488
parser = argparse.ArgumentParser(
14161489
description="Compares stubs to objects introspected from the runtime."
14171490
)
@@ -1469,13 +1542,24 @@ def parse_options(args: List[str]) -> argparse.Namespace:
14691542
parser.add_argument(
14701543
"--check-typeshed", action="store_true", help="Check all stdlib modules in typeshed"
14711544
)
1545+
parser.add_argument(
1546+
"--no-error-summary", action="store_false", dest="error_summary",
1547+
help="Don't output an error summary"
1548+
)
14721549

1473-
return parser.parse_args(args)
1550+
return parser.parse_args(args, namespace=Arguments())
14741551

14751552

14761553
def main() -> int:
14771554
mypy.util.check_python_version("stubtest")
1478-
return test_stubs(parse_options(sys.argv[1:]))
1555+
try:
1556+
return test_stubs(parse_options(sys.argv[1:]))
1557+
except KeyboardInterrupt:
1558+
return log_error("Interrupted")
1559+
except Failure:
1560+
return log_error("Stubtest has failed and exited early")
1561+
except Exception:
1562+
return log_error("Internal error encountered")
14791563

14801564

14811565
if __name__ == "__main__":

0 commit comments

Comments
 (0)