9
9
import enum
10
10
import importlib
11
11
import inspect
12
+ import io
12
13
import re
13
14
import sys
14
15
import types
15
16
import warnings
17
+ from contextlib import redirect_stdout , redirect_stderr
16
18
from functools import singledispatch
17
19
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
19
21
20
- from typing_extensions import Type
22
+ from typing_extensions import Type , Literal
21
23
22
24
import mypy .build
23
25
import mypy .modulefinder
26
28
import mypy .version
27
29
from mypy import nodes
28
30
from mypy .config_parser import parse_config_file
31
+ from mypy .messages import plural_s
29
32
from mypy .options import Options
30
33
from mypy .util import FancyFormatter , bytes_to_human_readable_repr , is_dunder
31
34
@@ -51,6 +54,16 @@ def _style(message: str, **kwargs: Any) -> str:
51
54
return _formatter .style (message , ** kwargs )
52
55
53
56
57
+ def log_error (message : str ) -> Literal [1 ]:
58
+ """Print a bold red message."""
59
+ print (_style (message , color = "red" , bold = True ))
60
+ return 1
61
+
62
+
63
+ class Failure (Exception ):
64
+ """Used to indicate a handled failure state"""
65
+
66
+
54
67
class Error :
55
68
def __init__ (
56
69
self ,
@@ -151,7 +164,7 @@ def get_description(self, concise: bool = False) -> str:
151
164
# ====================
152
165
153
166
154
- def test_module (module_name : str ) -> Iterator [Error ]:
167
+ def test_module (module_name : str , concise : bool = False ) -> Iterator [Error ]:
155
168
"""Tests a given module's stub against introspecting it at runtime.
156
169
157
170
Requires the stub to have been built already, accomplished by a call to ``build_stubs``.
@@ -161,20 +174,40 @@ def test_module(module_name: str) -> Iterator[Error]:
161
174
"""
162
175
stub = get_stub (module_name )
163
176
if stub is None :
164
- yield Error ([module_name ], "failed to find stubs" , MISSING , None )
177
+ yield Error ([module_name ], "failed to find stubs" , MISSING , None , runtime_desc = "N/A" )
165
178
return
166
179
180
+ argv = sys .argv
181
+ sys .argv = []
182
+ output = io .StringIO ()
183
+ outerror = io .StringIO ()
167
184
try :
168
- with warnings .catch_warnings ():
185
+ with warnings .catch_warnings (), redirect_stdout ( output ), redirect_stderr ( outerror ) :
169
186
warnings .simplefilter ("ignore" )
170
187
runtime = importlib .import_module (module_name )
171
188
# Also run the equivalent of `from module import *`
172
189
# This could have the additional effect of loading not-yet-loaded submodules
173
190
# mentioned in __all__
174
191
__import__ (module_name , fromlist = ["*" ])
175
- except Exception as e :
176
- yield Error ([module_name ], f"failed to import: { e } " , stub , MISSING )
192
+ except KeyboardInterrupt :
193
+ raise
194
+ except BaseException as e : # to catch every possible error
195
+ yield Error ([module_name ], f"failed to import: { type (e ).__name__ } { e } " , stub , MISSING ,
196
+ stub_desc = stub .path , runtime_desc = "Missing due to failed import" )
177
197
return
198
+ finally :
199
+ sys .argv = argv
200
+ stdout = output .getvalue ()
201
+ stderr = outerror .getvalue ()
202
+ if stdout or stderr and not concise :
203
+ print (f"Found output while loading '{ module_name } '" )
204
+ if stdout :
205
+ print (_style ("======= standard output ============" , bold = True ))
206
+ print (stdout , end = "" if stdout [- 1 ] == "\n " else "\n " )
207
+ if stderr :
208
+ print (_style ("======= standard error =============" , bold = True ))
209
+ print (stderr , end = "" if stderr [- 1 ] == "\n " else "\n " )
210
+ print (_style ("====================================" , bold = True ))
178
211
179
212
with warnings .catch_warnings ():
180
213
warnings .simplefilter ("ignore" )
@@ -461,21 +494,21 @@ def get_name(arg: Any) -> str:
461
494
return arg .name
462
495
if isinstance (arg , nodes .Argument ):
463
496
return arg .variable .name
464
- raise AssertionError
497
+ raise Failure
465
498
466
499
def get_type (arg : Any ) -> Optional [str ]:
467
500
if isinstance (arg , inspect .Parameter ):
468
501
return None
469
502
if isinstance (arg , nodes .Argument ):
470
503
return str (arg .variable .type or arg .type_annotation )
471
- raise AssertionError
504
+ raise Failure
472
505
473
506
def has_default (arg : Any ) -> bool :
474
507
if isinstance (arg , inspect .Parameter ):
475
508
return arg .default != inspect .Parameter .empty
476
509
if isinstance (arg , nodes .Argument ):
477
510
return arg .kind .is_optional ()
478
- raise AssertionError
511
+ raise Failure
479
512
480
513
def get_desc (arg : Any ) -> str :
481
514
arg_type = get_type (arg )
@@ -510,7 +543,7 @@ def from_funcitem(stub: nodes.FuncItem) -> "Signature[nodes.Argument]":
510
543
elif stub_arg .kind == nodes .ARG_STAR2 :
511
544
stub_sig .varkw = stub_arg
512
545
else :
513
- raise AssertionError
546
+ raise Failure
514
547
return stub_sig
515
548
516
549
@staticmethod
@@ -529,7 +562,7 @@ def from_inspect_signature(signature: inspect.Signature) -> "Signature[inspect.P
529
562
elif runtime_arg .kind == inspect .Parameter .VAR_KEYWORD :
530
563
runtime_sig .varkw = runtime_arg
531
564
else :
532
- raise AssertionError
565
+ raise Failure
533
566
return runtime_sig
534
567
535
568
@staticmethod
@@ -608,7 +641,7 @@ def get_kind(arg_name: str) -> nodes.ArgKind:
608
641
elif arg .kind == nodes .ARG_STAR2 :
609
642
sig .varkw = arg
610
643
else :
611
- raise AssertionError
644
+ raise Failure
612
645
return sig
613
646
614
647
@@ -918,7 +951,9 @@ def apply_decorator_to_funcitem(
918
951
) or decorator .fullname in mypy .types .OVERLOAD_NAMES :
919
952
return func
920
953
if decorator .fullname == "builtins.classmethod" :
921
- assert func .arguments [0 ].variable .name in ("cls" , "metacls" )
954
+ if func .arguments [0 ].variable .name not in ("cls" , "mcs" , "metacls" ):
955
+ log_error (f"Error: bad class argument name: { func .arguments [0 ].variable .name } " )
956
+ raise Failure
922
957
# FuncItem is written so that copy.copy() actually works, even when compiled
923
958
ret = copy .copy (func )
924
959
# Remove the cls argument, since it's not present in inspect.signature of classmethods
@@ -1156,7 +1191,7 @@ def anytype() -> mypy.types.AnyType:
1156
1191
elif arg .kind == inspect .Parameter .VAR_KEYWORD :
1157
1192
arg_kinds .append (nodes .ARG_STAR2 )
1158
1193
else :
1159
- raise AssertionError
1194
+ raise Failure
1160
1195
else :
1161
1196
arg_types = [anytype (), anytype ()]
1162
1197
arg_kinds = [nodes .ARG_STAR , nodes .ARG_STAR2 ]
@@ -1257,14 +1292,14 @@ def build_stubs(modules: List[str], options: Options, find_submodules: bool = Fa
1257
1292
str (e ),
1258
1293
]
1259
1294
print ("" .join (output ))
1260
- raise RuntimeError from e
1295
+ raise Failure from e
1261
1296
if res .errors :
1262
1297
output = [
1263
1298
_style ("error: " , color = "red" , bold = True ),
1264
1299
"not checking stubs due to mypy build errors:\n " ,
1265
1300
]
1266
1301
print ("" .join (output ) + "\n " .join (res .errors ))
1267
- raise RuntimeError
1302
+ raise Failure
1268
1303
1269
1304
global _all_stubs
1270
1305
_all_stubs = res .files
@@ -1274,7 +1309,10 @@ def build_stubs(modules: List[str], options: Options, find_submodules: bool = Fa
1274
1309
1275
1310
def get_stub (module : str ) -> Optional [nodes .MypyFile ]:
1276
1311
"""Returns a stub object for the given module, if we've built one."""
1277
- return _all_stubs .get (module )
1312
+ result = _all_stubs .get (module )
1313
+ if result and result .is_stub :
1314
+ return result
1315
+ return None
1278
1316
1279
1317
1280
1318
def get_typeshed_stdlib_modules (
@@ -1329,7 +1367,22 @@ def strip_comments(s: str) -> str:
1329
1367
yield entry
1330
1368
1331
1369
1332
- def test_stubs (args : argparse .Namespace , use_builtins_fixtures : bool = False ) -> int :
1370
+ class Arguments :
1371
+ modules : List [str ]
1372
+ concise : bool
1373
+ ignore_missing_stub : bool
1374
+ ignore_positional_only : bool
1375
+ allowlist : List [str ]
1376
+ generate_allowlist : bool
1377
+ ignore_unused_allowlist : bool
1378
+ mypy_config_file : str
1379
+ custom_typeshed_dir : str
1380
+ check_typeshed : bool
1381
+ version : str
1382
+ error_summary : bool
1383
+
1384
+
1385
+ def test_stubs (args : Arguments , use_builtins_fixtures : bool = False ) -> int :
1333
1386
"""This is stubtest! It's time to test the stubs!"""
1334
1387
# Load the allowlist. This is a series of strings corresponding to Error.object_desc
1335
1388
# Values in the dict will store whether we used the allowlist entry or not.
@@ -1345,13 +1398,15 @@ def test_stubs(args: argparse.Namespace, use_builtins_fixtures: bool = False) ->
1345
1398
1346
1399
modules = args .modules
1347
1400
if args .check_typeshed :
1348
- assert not args .modules , "Cannot pass both --check-typeshed and a list of modules"
1401
+ if args .modules :
1402
+ return log_error ("Cannot pass both --check-typeshed and a list of modules" )
1349
1403
modules = get_typeshed_stdlib_modules (args .custom_typeshed_dir )
1350
1404
# typeshed added a stub for __main__, but that causes stubtest to check itself
1351
1405
annoying_modules = {"antigravity" , "this" , "__main__" }
1352
1406
modules = [m for m in modules if m not in annoying_modules ]
1353
1407
1354
- assert modules , "No modules to check"
1408
+ if not modules :
1409
+ return log_error ("No modules to check" )
1355
1410
1356
1411
options = Options ()
1357
1412
options .incremental = False
@@ -1366,12 +1421,14 @@ def set_strict_flags() -> None: # not needed yet
1366
1421
1367
1422
try :
1368
1423
modules = build_stubs (modules , options , find_submodules = not args .check_typeshed )
1369
- except RuntimeError :
1424
+ except Failure :
1370
1425
return 1
1371
1426
1372
1427
exit_code = 0
1428
+ error_count = 0
1429
+ error_modules : Set [str ] = set ()
1373
1430
for module in modules :
1374
- for error in test_module (module ):
1431
+ for error in test_module (module , args . concise ):
1375
1432
# Filter errors
1376
1433
if args .ignore_missing_stub and error .is_missing_stub ():
1377
1434
continue
@@ -1395,6 +1452,8 @@ def set_strict_flags() -> None: # not needed yet
1395
1452
generated_allowlist .add (error .object_desc )
1396
1453
continue
1397
1454
print (error .get_description (concise = args .concise ))
1455
+ error_count += 1
1456
+ error_modules .add (module )
1398
1457
1399
1458
# Print unused allowlist entries
1400
1459
if not args .ignore_unused_allowlist :
@@ -1411,10 +1470,25 @@ def set_strict_flags() -> None: # not needed yet
1411
1470
print (e )
1412
1471
exit_code = 0
1413
1472
1473
+ if args .error_summary :
1474
+ if not error_count :
1475
+ print (
1476
+ _style (
1477
+ f"Success: no issues found in { len (modules )} module{ plural_s (modules )} " ,
1478
+ color = "green" , bold = True
1479
+ )
1480
+ )
1481
+ else :
1482
+ log_error (
1483
+ f"Found { error_count } error{ plural_s (error_count )} in { len (error_modules )} "
1484
+ f" module{ plural_s (error_modules )} "
1485
+ f" (checked { len (modules )} module{ plural_s (modules )} )"
1486
+ )
1487
+
1414
1488
return exit_code
1415
1489
1416
1490
1417
- def parse_options (args : List [str ]) -> argparse . Namespace :
1491
+ def parse_options (args : List [str ]) -> Arguments :
1418
1492
parser = argparse .ArgumentParser (
1419
1493
description = "Compares stubs to objects introspected from the runtime."
1420
1494
)
@@ -1475,13 +1549,24 @@ def parse_options(args: List[str]) -> argparse.Namespace:
1475
1549
parser .add_argument (
1476
1550
"--version" , action = "version" , version = "%(prog)s " + mypy .version .__version__
1477
1551
)
1552
+ parser .add_argument (
1553
+ "--no-error-summary" , action = "store_false" , dest = "error_summary" ,
1554
+ help = "Don't output an error summary"
1555
+ )
1478
1556
1479
- return parser .parse_args (args )
1557
+ return parser .parse_args (args , namespace = Arguments () )
1480
1558
1481
1559
1482
1560
def main () -> int :
1483
1561
mypy .util .check_python_version ("stubtest" )
1484
- return test_stubs (parse_options (sys .argv [1 :]))
1562
+ try :
1563
+ return test_stubs (parse_options (sys .argv [1 :]))
1564
+ except KeyboardInterrupt :
1565
+ return log_error ("Interrupted" )
1566
+ except Failure :
1567
+ return log_error ("Stubtest has failed and exited early" )
1568
+ except Exception :
1569
+ return log_error ("Internal error encountered" )
1485
1570
1486
1571
1487
1572
if __name__ == "__main__" :
0 commit comments