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
24
26
import mypy .state
25
27
import mypy .types
26
28
from mypy import nodes
27
29
from mypy .config_parser import parse_config_file
30
+ from mypy .messages import plural_s
28
31
from mypy .options import Options
29
32
from mypy .util import FancyFormatter , bytes_to_human_readable_repr , is_dunder
30
33
@@ -50,6 +53,16 @@ def _style(message: str, **kwargs: Any) -> str:
50
53
return _formatter .style (message , ** kwargs )
51
54
52
55
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
+
53
66
class Error :
54
67
def __init__ (
55
68
self ,
@@ -150,7 +163,7 @@ def get_description(self, concise: bool = False) -> str:
150
163
# ====================
151
164
152
165
153
- def test_module (module_name : str ) -> Iterator [Error ]:
166
+ def test_module (module_name : str , concise : bool = False ) -> Iterator [Error ]:
154
167
"""Tests a given module's stub against introspecting it at runtime.
155
168
156
169
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]:
160
173
"""
161
174
stub = get_stub (module_name )
162
175
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" )
164
177
return
165
178
179
+ argv = sys .argv
180
+ sys .argv = []
181
+ output = io .StringIO ()
182
+ outerror = io .StringIO ()
166
183
try :
167
- with warnings .catch_warnings ():
184
+ with warnings .catch_warnings (), redirect_stdout ( output ), redirect_stderr ( outerror ) :
168
185
warnings .simplefilter ("ignore" )
169
186
runtime = importlib .import_module (module_name )
170
187
# Also run the equivalent of `from module import *`
171
188
# This could have the additional effect of loading not-yet-loaded submodules
172
189
# mentioned in __all__
173
190
__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" )
176
196
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 ))
177
210
178
211
with warnings .catch_warnings ():
179
212
warnings .simplefilter ("ignore" )
@@ -458,21 +491,21 @@ def get_name(arg: Any) -> str:
458
491
return arg .name
459
492
if isinstance (arg , nodes .Argument ):
460
493
return arg .variable .name
461
- raise AssertionError
494
+ raise Failure
462
495
463
496
def get_type (arg : Any ) -> Optional [str ]:
464
497
if isinstance (arg , inspect .Parameter ):
465
498
return None
466
499
if isinstance (arg , nodes .Argument ):
467
500
return str (arg .variable .type or arg .type_annotation )
468
- raise AssertionError
501
+ raise Failure
469
502
470
503
def has_default (arg : Any ) -> bool :
471
504
if isinstance (arg , inspect .Parameter ):
472
505
return arg .default != inspect .Parameter .empty
473
506
if isinstance (arg , nodes .Argument ):
474
507
return arg .kind .is_optional ()
475
- raise AssertionError
508
+ raise Failure
476
509
477
510
def get_desc (arg : Any ) -> str :
478
511
arg_type = get_type (arg )
@@ -507,7 +540,7 @@ def from_funcitem(stub: nodes.FuncItem) -> "Signature[nodes.Argument]":
507
540
elif stub_arg .kind == nodes .ARG_STAR2 :
508
541
stub_sig .varkw = stub_arg
509
542
else :
510
- raise AssertionError
543
+ raise Failure
511
544
return stub_sig
512
545
513
546
@staticmethod
@@ -526,7 +559,7 @@ def from_inspect_signature(signature: inspect.Signature) -> "Signature[inspect.P
526
559
elif runtime_arg .kind == inspect .Parameter .VAR_KEYWORD :
527
560
runtime_sig .varkw = runtime_arg
528
561
else :
529
- raise AssertionError
562
+ raise Failure
530
563
return runtime_sig
531
564
532
565
@staticmethod
@@ -605,7 +638,7 @@ def get_kind(arg_name: str) -> nodes.ArgKind:
605
638
elif arg .kind == nodes .ARG_STAR2 :
606
639
sig .varkw = arg
607
640
else :
608
- raise AssertionError
641
+ raise Failure
609
642
return sig
610
643
611
644
@@ -915,7 +948,9 @@ def apply_decorator_to_funcitem(
915
948
) or decorator .fullname in mypy .types .OVERLOAD_NAMES :
916
949
return func
917
950
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
919
954
# FuncItem is written so that copy.copy() actually works, even when compiled
920
955
ret = copy .copy (func )
921
956
# Remove the cls argument, since it's not present in inspect.signature of classmethods
@@ -1153,7 +1188,7 @@ def anytype() -> mypy.types.AnyType:
1153
1188
elif arg .kind == inspect .Parameter .VAR_KEYWORD :
1154
1189
arg_kinds .append (nodes .ARG_STAR2 )
1155
1190
else :
1156
- raise AssertionError
1191
+ raise Failure
1157
1192
else :
1158
1193
arg_types = [anytype (), anytype ()]
1159
1194
arg_kinds = [nodes .ARG_STAR , nodes .ARG_STAR2 ]
@@ -1254,14 +1289,14 @@ def build_stubs(modules: List[str], options: Options, find_submodules: bool = Fa
1254
1289
str (e ),
1255
1290
]
1256
1291
print ("" .join (output ))
1257
- raise RuntimeError from e
1292
+ raise Failure from e
1258
1293
if res .errors :
1259
1294
output = [
1260
1295
_style ("error: " , color = "red" , bold = True ),
1261
1296
"not checking stubs due to mypy build errors:\n " ,
1262
1297
]
1263
1298
print ("" .join (output ) + "\n " .join (res .errors ))
1264
- raise RuntimeError
1299
+ raise Failure
1265
1300
1266
1301
global _all_stubs
1267
1302
_all_stubs = res .files
@@ -1271,7 +1306,10 @@ def build_stubs(modules: List[str], options: Options, find_submodules: bool = Fa
1271
1306
1272
1307
def get_stub (module : str ) -> Optional [nodes .MypyFile ]:
1273
1308
"""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
1275
1313
1276
1314
1277
1315
def get_typeshed_stdlib_modules (
@@ -1326,7 +1364,21 @@ def strip_comments(s: str) -> str:
1326
1364
yield entry
1327
1365
1328
1366
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 :
1330
1382
"""This is stubtest! It's time to test the stubs!"""
1331
1383
# Load the allowlist. This is a series of strings corresponding to Error.object_desc
1332
1384
# 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) ->
1342
1394
1343
1395
modules = args .modules
1344
1396
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" )
1346
1399
modules = get_typeshed_stdlib_modules (args .custom_typeshed_dir )
1347
1400
# typeshed added a stub for __main__, but that causes stubtest to check itself
1348
1401
annoying_modules = {"antigravity" , "this" , "__main__" }
1349
1402
modules = [m for m in modules if m not in annoying_modules ]
1350
1403
1351
- assert modules , "No modules to check"
1404
+ if not modules :
1405
+ return log_error ("No modules to check" )
1352
1406
1353
1407
options = Options ()
1354
1408
options .incremental = False
@@ -1363,12 +1417,14 @@ def set_strict_flags() -> None: # not needed yet
1363
1417
1364
1418
try :
1365
1419
modules = build_stubs (modules , options , find_submodules = not args .check_typeshed )
1366
- except RuntimeError :
1420
+ except Failure :
1367
1421
return 1
1368
1422
1369
1423
exit_code = 0
1424
+ error_count = 0
1425
+ error_modules : Set [str ] = set ()
1370
1426
for module in modules :
1371
- for error in test_module (module ):
1427
+ for error in test_module (module , args . concise ):
1372
1428
# Filter errors
1373
1429
if args .ignore_missing_stub and error .is_missing_stub ():
1374
1430
continue
@@ -1392,6 +1448,8 @@ def set_strict_flags() -> None: # not needed yet
1392
1448
generated_allowlist .add (error .object_desc )
1393
1449
continue
1394
1450
print (error .get_description (concise = args .concise ))
1451
+ error_count += 1
1452
+ error_modules .add (module )
1395
1453
1396
1454
# Print unused allowlist entries
1397
1455
if not args .ignore_unused_allowlist :
@@ -1408,10 +1466,25 @@ def set_strict_flags() -> None: # not needed yet
1408
1466
print (e )
1409
1467
exit_code = 0
1410
1468
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
+
1411
1484
return exit_code
1412
1485
1413
1486
1414
- def parse_options (args : List [str ]) -> argparse . Namespace :
1487
+ def parse_options (args : List [str ]) -> Arguments :
1415
1488
parser = argparse .ArgumentParser (
1416
1489
description = "Compares stubs to objects introspected from the runtime."
1417
1490
)
@@ -1469,13 +1542,24 @@ def parse_options(args: List[str]) -> argparse.Namespace:
1469
1542
parser .add_argument (
1470
1543
"--check-typeshed" , action = "store_true" , help = "Check all stdlib modules in typeshed"
1471
1544
)
1545
+ parser .add_argument (
1546
+ "--no-error-summary" , action = "store_false" , dest = "error_summary" ,
1547
+ help = "Don't output an error summary"
1548
+ )
1472
1549
1473
- return parser .parse_args (args )
1550
+ return parser .parse_args (args , namespace = Arguments () )
1474
1551
1475
1552
1476
1553
def main () -> int :
1477
1554
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" )
1479
1563
1480
1564
1481
1565
if __name__ == "__main__" :
0 commit comments