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 typing
16
17
import typing_extensions
17
18
import warnings
19
+ from contextlib import redirect_stdout , redirect_stderr
18
20
from functools import singledispatch
19
21
from pathlib import Path
20
22
from typing import Any , Dict , Generic , Iterator , List , Optional , Tuple , TypeVar , Union , cast
29
31
from mypy import nodes
30
32
from mypy .config_parser import parse_config_file
31
33
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
33
35
34
36
35
37
class Missing :
@@ -53,6 +55,10 @@ def _style(message: str, **kwargs: Any) -> str:
53
55
return _formatter .style (message , ** kwargs )
54
56
55
57
58
+ class StubtestFailure (Exception ):
59
+ pass
60
+
61
+
56
62
class Error :
57
63
def __init__ (
58
64
self ,
@@ -163,19 +169,20 @@ def test_module(module_name: str) -> Iterator[Error]:
163
169
"""
164
170
stub = get_stub (module_name )
165
171
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" )
167
173
return
168
174
169
175
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 = ["*" ])
177
184
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 )
179
186
return
180
187
181
188
with warnings .catch_warnings ():
@@ -944,7 +951,11 @@ def apply_decorator_to_funcitem(
944
951
) or decorator .fullname in mypy .types .OVERLOAD_NAMES :
945
952
return func
946
953
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
+ )
948
959
# FuncItem is written so that copy.copy() actually works, even when compiled
949
960
ret = copy .copy (func )
950
961
# 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
1274
1285
sources .extend (found_sources )
1275
1286
all_modules .extend (s .module for s in found_sources if s .module not in all_modules )
1276
1287
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 ))
1294
1295
1295
- global _all_stubs
1296
- _all_stubs = res .files
1296
+ global _all_stubs
1297
+ _all_stubs = res .files
1297
1298
1298
1299
return all_modules
1299
1300
@@ -1355,7 +1356,21 @@ def strip_comments(s: str) -> str:
1355
1356
yield entry
1356
1357
1357
1358
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 :
1359
1374
"""This is stubtest! It's time to test the stubs!"""
1360
1375
# Load the allowlist. This is a series of strings corresponding to Error.object_desc
1361
1376
# 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) ->
1371
1386
1372
1387
modules = args .modules
1373
1388
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
1375
1395
modules = get_typeshed_stdlib_modules (args .custom_typeshed_dir )
1376
1396
# typeshed added a stub for __main__, but that causes stubtest to check itself
1377
1397
annoying_modules = {"antigravity" , "this" , "__main__" }
1378
1398
modules = [m for m in modules if m not in annoying_modules ]
1379
1399
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
1381
1406
1382
1407
options = Options ()
1383
1408
options .incremental = False
@@ -1392,10 +1417,15 @@ def set_strict_flags() -> None: # not needed yet
1392
1417
1393
1418
try :
1394
1419
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
+ )
1396
1425
return 1
1397
1426
1398
1427
exit_code = 0
1428
+ error_count = 0
1399
1429
for module in modules :
1400
1430
for error in test_module (module ):
1401
1431
# Filter errors
@@ -1421,6 +1451,7 @@ def set_strict_flags() -> None: # not needed yet
1421
1451
generated_allowlist .add (error .object_desc )
1422
1452
continue
1423
1453
print (error .get_description (concise = args .concise ))
1454
+ error_count += 1
1424
1455
1425
1456
# Print unused allowlist entries
1426
1457
if not args .ignore_unused_allowlist :
@@ -1429,18 +1460,35 @@ def set_strict_flags() -> None: # not needed yet
1429
1460
# This lets us allowlist errors that don't manifest at all on some systems
1430
1461
if not allowlist [w ] and not allowlist_regexes [w ].fullmatch ("" ):
1431
1462
exit_code = 1
1463
+ error_count += 1
1432
1464
print (f"note: unused allowlist entry { w } " )
1433
1465
1434
1466
# Print the generated allowlist
1435
1467
if args .generate_allowlist :
1436
1468
for e in sorted (generated_allowlist ):
1437
1469
print (e )
1438
1470
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
+ )
1439
1487
1440
1488
return exit_code
1441
1489
1442
1490
1443
- def parse_options (args : List [str ]) -> argparse . Namespace :
1491
+ def parse_options (args : List [str ]) -> _Arguments :
1444
1492
parser = argparse .ArgumentParser (
1445
1493
description = "Compares stubs to objects introspected from the runtime."
1446
1494
)
@@ -1502,7 +1550,7 @@ def parse_options(args: List[str]) -> argparse.Namespace:
1502
1550
"--version" , action = "version" , version = "%(prog)s " + mypy .version .__version__
1503
1551
)
1504
1552
1505
- return parser .parse_args (args )
1553
+ return parser .parse_args (args , namespace = _Arguments () )
1506
1554
1507
1555
1508
1556
def main () -> int :
0 commit comments