9
9
import enum
10
10
import importlib
11
11
import inspect
12
+ import os
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
20
from typing import Any , Dict , Generic , Iterator , List , Optional , Tuple , TypeVar , Union , cast
27
29
from mypy import nodes
28
30
from mypy .config_parser import parse_config_file
29
31
from mypy .options import Options
30
- from mypy .util import FancyFormatter , bytes_to_human_readable_repr , is_dunder
32
+ from mypy .util import FancyFormatter , bytes_to_human_readable_repr , plural_s , is_dunder
31
33
32
34
33
35
class Missing :
@@ -51,6 +53,10 @@ def _style(message: str, **kwargs: Any) -> str:
51
53
return _formatter .style (message , ** kwargs )
52
54
53
55
56
+ class StubtestFailure (Exception ):
57
+ pass
58
+
59
+
54
60
class Error :
55
61
def __init__ (
56
62
self ,
@@ -161,19 +167,20 @@ def test_module(module_name: str) -> Iterator[Error]:
161
167
"""
162
168
stub = get_stub (module_name )
163
169
if stub is None :
164
- yield Error ([module_name ], "failed to find stubs" , MISSING , None )
170
+ yield Error ([module_name ], "failed to find stubs" , MISSING , None , runtime_desc = "N/A" )
165
171
return
166
172
167
173
try :
168
- with warnings .catch_warnings ():
169
- warnings .simplefilter ("ignore" )
170
- runtime = importlib .import_module (module_name )
171
- # Also run the equivalent of `from module import *`
172
- # This could have the additional effect of loading not-yet-loaded submodules
173
- # mentioned in __all__
174
- __import__ (module_name , fromlist = ["*" ])
174
+ with open (os .devnull , "w" ) as devnull :
175
+ with warnings .catch_warnings (), redirect_stdout (devnull ), redirect_stderr (devnull ):
176
+ warnings .simplefilter ("ignore" )
177
+ runtime = importlib .import_module (module_name )
178
+ # Also run the equivalent of `from module import *`
179
+ # This could have the additional effect of loading not-yet-loaded submodules
180
+ # mentioned in __all__
181
+ __import__ (module_name , fromlist = ["*" ])
175
182
except Exception as e :
176
- yield Error ([module_name ], f"failed to import: { e } " , stub , MISSING )
183
+ yield Error ([module_name ], f"failed to import, { type ( e ). __name__ } : { e } " , stub , MISSING )
177
184
return
178
185
179
186
with warnings .catch_warnings ():
@@ -918,7 +925,11 @@ def apply_decorator_to_funcitem(
918
925
) or decorator .fullname in mypy .types .OVERLOAD_NAMES :
919
926
return func
920
927
if decorator .fullname == "builtins.classmethod" :
921
- assert func .arguments [0 ].variable .name in ("cls" , "metacls" )
928
+ if func .arguments [0 ].variable .name not in ("cls" , "mcs" , "metacls" ):
929
+ raise StubtestFailure (
930
+ f"unexpected class argument name { func .arguments [0 ].variable .name !r} "
931
+ f"in { dec .fullname } "
932
+ )
922
933
# FuncItem is written so that copy.copy() actually works, even when compiled
923
934
ret = copy .copy (func )
924
935
# Remove the cls argument, since it's not present in inspect.signature of classmethods
@@ -1251,20 +1262,9 @@ def build_stubs(modules: List[str], options: Options, find_submodules: bool = Fa
1251
1262
try :
1252
1263
res = mypy .build .build (sources = sources , options = options )
1253
1264
except mypy .errors .CompileError as e :
1254
- output = [
1255
- _style ("error: " , color = "red" , bold = True ),
1256
- "not checking stubs due to failed mypy compile:\n " ,
1257
- str (e ),
1258
- ]
1259
- print ("" .join (output ))
1260
- raise RuntimeError from e
1265
+ raise StubtestFailure (f"failed mypy compile:\n { e } " ) from e
1261
1266
if res .errors :
1262
- output = [
1263
- _style ("error: " , color = "red" , bold = True ),
1264
- "not checking stubs due to mypy build errors:\n " ,
1265
- ]
1266
- print ("" .join (output ) + "\n " .join (res .errors ))
1267
- raise RuntimeError
1267
+ raise StubtestFailure ("mypy build errors:\n " + "\n " .join (res .errors ))
1268
1268
1269
1269
global _all_stubs
1270
1270
_all_stubs = res .files
@@ -1329,7 +1329,21 @@ def strip_comments(s: str) -> str:
1329
1329
yield entry
1330
1330
1331
1331
1332
- def test_stubs (args : argparse .Namespace , use_builtins_fixtures : bool = False ) -> int :
1332
+ class _Arguments :
1333
+ modules : List [str ]
1334
+ concise : bool
1335
+ ignore_missing_stub : bool
1336
+ ignore_positional_only : bool
1337
+ allowlist : List [str ]
1338
+ generate_allowlist : bool
1339
+ ignore_unused_allowlist : bool
1340
+ mypy_config_file : str
1341
+ custom_typeshed_dir : str
1342
+ check_typeshed : bool
1343
+ version : str
1344
+
1345
+
1346
+ def test_stubs (args : _Arguments , use_builtins_fixtures : bool = False ) -> int :
1333
1347
"""This is stubtest! It's time to test the stubs!"""
1334
1348
# Load the allowlist. This is a series of strings corresponding to Error.object_desc
1335
1349
# Values in the dict will store whether we used the allowlist entry or not.
@@ -1345,13 +1359,23 @@ def test_stubs(args: argparse.Namespace, use_builtins_fixtures: bool = False) ->
1345
1359
1346
1360
modules = args .modules
1347
1361
if args .check_typeshed :
1348
- assert not args .modules , "Cannot pass both --check-typeshed and a list of modules"
1362
+ if args .modules :
1363
+ print (
1364
+ _style ("error:" , color = "red" , bold = True ),
1365
+ "cannot pass both --check-typeshed and a list of modules" ,
1366
+ )
1367
+ return 1
1349
1368
modules = get_typeshed_stdlib_modules (args .custom_typeshed_dir )
1350
1369
# typeshed added a stub for __main__, but that causes stubtest to check itself
1351
1370
annoying_modules = {"antigravity" , "this" , "__main__" }
1352
1371
modules = [m for m in modules if m not in annoying_modules ]
1353
1372
1354
- assert modules , "No modules to check"
1373
+ if not modules :
1374
+ print (
1375
+ _style ("error:" , color = "red" , bold = True ),
1376
+ "no modules to check" ,
1377
+ )
1378
+ return 1
1355
1379
1356
1380
options = Options ()
1357
1381
options .incremental = False
@@ -1366,10 +1390,15 @@ def set_strict_flags() -> None: # not needed yet
1366
1390
1367
1391
try :
1368
1392
modules = build_stubs (modules , options , find_submodules = not args .check_typeshed )
1369
- except RuntimeError :
1393
+ except StubtestFailure as stubtest_failure :
1394
+ print (
1395
+ _style ("error:" , color = "red" , bold = True ),
1396
+ f"not checking stubs due to { stubtest_failure } " ,
1397
+ )
1370
1398
return 1
1371
1399
1372
1400
exit_code = 0
1401
+ error_count = 0
1373
1402
for module in modules :
1374
1403
for error in test_module (module ):
1375
1404
# Filter errors
@@ -1395,6 +1424,7 @@ def set_strict_flags() -> None: # not needed yet
1395
1424
generated_allowlist .add (error .object_desc )
1396
1425
continue
1397
1426
print (error .get_description (concise = args .concise ))
1427
+ error_count += 1
1398
1428
1399
1429
# Print unused allowlist entries
1400
1430
if not args .ignore_unused_allowlist :
@@ -1403,18 +1433,35 @@ def set_strict_flags() -> None: # not needed yet
1403
1433
# This lets us allowlist errors that don't manifest at all on some systems
1404
1434
if not allowlist [w ] and not allowlist_regexes [w ].fullmatch ("" ):
1405
1435
exit_code = 1
1436
+ error_count += 1
1406
1437
print (f"note: unused allowlist entry { w } " )
1407
1438
1408
1439
# Print the generated allowlist
1409
1440
if args .generate_allowlist :
1410
1441
for e in sorted (generated_allowlist ):
1411
1442
print (e )
1412
1443
exit_code = 0
1444
+ elif not args .concise :
1445
+ if error_count :
1446
+ print (
1447
+ _style (
1448
+ f"Found { error_count } error{ plural_s (error_count )} "
1449
+ f" (checked { len (modules )} module{ plural_s (modules )} )" ,
1450
+ color = "red" , bold = True
1451
+ )
1452
+ )
1453
+ else :
1454
+ print (
1455
+ _style (
1456
+ f"Success: no issues found in { len (modules )} module{ plural_s (modules )} " ,
1457
+ color = "green" , bold = True
1458
+ )
1459
+ )
1413
1460
1414
1461
return exit_code
1415
1462
1416
1463
1417
- def parse_options (args : List [str ]) -> argparse . Namespace :
1464
+ def parse_options (args : List [str ]) -> _Arguments :
1418
1465
parser = argparse .ArgumentParser (
1419
1466
description = "Compares stubs to objects introspected from the runtime."
1420
1467
)
@@ -1476,7 +1523,7 @@ def parse_options(args: List[str]) -> argparse.Namespace:
1476
1523
"--version" , action = "version" , version = "%(prog)s " + mypy .version .__version__
1477
1524
)
1478
1525
1479
- return parser .parse_args (args )
1526
+ return parser .parse_args (args , namespace = _Arguments () )
1480
1527
1481
1528
1482
1529
def main () -> int :
0 commit comments