99import enum
1010import importlib
1111import inspect
12+ import os
1213import re
1314import sys
1415import types
1516import typing
1617import typing_extensions
1718import warnings
19+ from contextlib import redirect_stdout , redirect_stderr
1820from functools import singledispatch
1921from pathlib import Path
2022from typing import Any , Dict , Generic , Iterator , List , Optional , Tuple , TypeVar , Union , cast
2931from mypy import nodes
3032from mypy .config_parser import parse_config_file
3133from 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
3335
3436
3537class Missing :
@@ -53,6 +55,10 @@ def _style(message: str, **kwargs: Any) -> str:
5355 return _formatter .style (message , ** kwargs )
5456
5557
58+ class StubtestFailure (Exception ):
59+ pass
60+
61+
5662class Error :
5763 def __init__ (
5864 self ,
@@ -163,19 +169,20 @@ def test_module(module_name: str) -> Iterator[Error]:
163169 """
164170 stub = get_stub (module_name )
165171 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" )
167173 return
168174
169175 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 = ["*" ])
177184 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 )
179186 return
180187
181188 with warnings .catch_warnings ():
@@ -944,7 +951,11 @@ def apply_decorator_to_funcitem(
944951 ) or decorator .fullname in mypy .types .OVERLOAD_NAMES :
945952 return func
946953 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+ )
948959 # FuncItem is written so that copy.copy() actually works, even when compiled
949960 ret = copy .copy (func )
950961 # 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
12741285 sources .extend (found_sources )
12751286 all_modules .extend (s .module for s in found_sources if s .module not in all_modules )
12761287
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 ))
12941295
1295- global _all_stubs
1296- _all_stubs = res .files
1296+ global _all_stubs
1297+ _all_stubs = res .files
12971298
12981299 return all_modules
12991300
@@ -1355,7 +1356,21 @@ def strip_comments(s: str) -> str:
13551356 yield entry
13561357
13571358
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 :
13591374 """This is stubtest! It's time to test the stubs!"""
13601375 # Load the allowlist. This is a series of strings corresponding to Error.object_desc
13611376 # 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) ->
13711386
13721387 modules = args .modules
13731388 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
13751395 modules = get_typeshed_stdlib_modules (args .custom_typeshed_dir )
13761396 # typeshed added a stub for __main__, but that causes stubtest to check itself
13771397 annoying_modules = {"antigravity" , "this" , "__main__" }
13781398 modules = [m for m in modules if m not in annoying_modules ]
13791399
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
13811406
13821407 options = Options ()
13831408 options .incremental = False
@@ -1392,10 +1417,15 @@ def set_strict_flags() -> None: # not needed yet
13921417
13931418 try :
13941419 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+ )
13961425 return 1
13971426
13981427 exit_code = 0
1428+ error_count = 0
13991429 for module in modules :
14001430 for error in test_module (module ):
14011431 # Filter errors
@@ -1421,6 +1451,7 @@ def set_strict_flags() -> None: # not needed yet
14211451 generated_allowlist .add (error .object_desc )
14221452 continue
14231453 print (error .get_description (concise = args .concise ))
1454+ error_count += 1
14241455
14251456 # Print unused allowlist entries
14261457 if not args .ignore_unused_allowlist :
@@ -1429,18 +1460,35 @@ def set_strict_flags() -> None: # not needed yet
14291460 # This lets us allowlist errors that don't manifest at all on some systems
14301461 if not allowlist [w ] and not allowlist_regexes [w ].fullmatch ("" ):
14311462 exit_code = 1
1463+ error_count += 1
14321464 print (f"note: unused allowlist entry { w } " )
14331465
14341466 # Print the generated allowlist
14351467 if args .generate_allowlist :
14361468 for e in sorted (generated_allowlist ):
14371469 print (e )
14381470 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+ )
14391487
14401488 return exit_code
14411489
14421490
1443- def parse_options (args : List [str ]) -> argparse . Namespace :
1491+ def parse_options (args : List [str ]) -> _Arguments :
14441492 parser = argparse .ArgumentParser (
14451493 description = "Compares stubs to objects introspected from the runtime."
14461494 )
@@ -1502,7 +1550,7 @@ def parse_options(args: List[str]) -> argparse.Namespace:
15021550 "--version" , action = "version" , version = "%(prog)s " + mypy .version .__version__
15031551 )
15041552
1505- return parser .parse_args (args )
1553+ return parser .parse_args (args , namespace = _Arguments () )
15061554
15071555
15081556def main () -> int :
0 commit comments