2121from typing_extensions import Annotated , TypeAlias
2222
2323import tomli
24- from utils import colored , print_error , print_success_msg , read_dependencies
24+ from utils import VERSIONS_RE as VERSION_LINE_RE , colored , print_error , print_success_msg , read_dependencies , strip_comments
2525
26- SUPPORTED_VERSIONS = [( 3 , 11 ), ( 3 , 10 ), ( 3 , 9 ), ( 3 , 8 ), ( 3 , 7 ) ]
26+ SUPPORTED_VERSIONS = ["3.11" , "3.10" , "3.9" , "3.8" , "3.7" ]
2727SUPPORTED_PLATFORMS = ("linux" , "win32" , "darwin" )
28- TYPESHED_DIRECTORIES = frozenset ({ "stdlib" , "stubs" })
28+ DIRECTORIES_TO_TEST = [ Path ( "stdlib" ), Path ( "stubs" )]
2929
3030ReturnCode : TypeAlias = int
31- MajorVersion : TypeAlias = int
32- MinorVersion : TypeAlias = int
33- MinVersion : TypeAlias = tuple [MajorVersion , MinorVersion ]
34- MaxVersion : TypeAlias = tuple [MajorVersion , MinorVersion ]
31+ VersionString : TypeAlias = Annotated [str , "Must be one of the entries in SUPPORTED_VERSIONS" ]
32+ VersionTuple : TypeAlias = tuple [int , int ]
3533Platform : TypeAlias = Annotated [str , "Must be one of the entries in SUPPORTED_PLATFORMS" ]
36- Directory : TypeAlias = Annotated [str , "Must be one of the entries in TYPESHED_DIRECTORIES" ]
37-
38-
39- def python_version (arg : str ) -> tuple [MajorVersion , MinorVersion ]:
40- version = tuple (map (int , arg .split ("." ))) # This will naturally raise TypeError if it's not in the form "{major}.{minor}"
41- if version not in SUPPORTED_VERSIONS :
42- raise ValueError
43- # mypy infers the return type as tuple[int, ...]
44- return version # type: ignore[return-value]
4534
4635
4736class CommandLineArgs (argparse .Namespace ):
4837 verbose : int
49- exclude : list [str ] | None
50- python_version : list [tuple [ MajorVersion , MinorVersion ] ] | None
51- dir : list [Directory ] | None
38+ filter : list [Path ]
39+ exclude : list [Path ] | None
40+ python_version : list [VersionString ] | None
5241 platform : list [Platform ] | None
53- filter : list [str ]
42+
43+
44+ def valid_path (cmd_arg : str ) -> Path :
45+ """Helper function for argument-parsing"""
46+ path = Path (cmd_arg )
47+ if not path .exists ():
48+ raise argparse .ArgumentTypeError (f'"{ path } " does not exist in typeshed!' )
49+ if not (path in DIRECTORIES_TO_TEST or any (directory in path .parents for directory in DIRECTORIES_TO_TEST )):
50+ raise argparse .ArgumentTypeError ('mypy_test.py only tests the stubs found in the "stdlib" and "stubs" directories' )
51+ return path
5452
5553
5654parser = argparse .ArgumentParser (
5755 description = "Typecheck typeshed's stubs with mypy. Patterns are unanchored regexps on the full path."
5856)
59- parser .add_argument ("-v" , "--verbose" , action = "count" , default = 0 , help = "More output" )
60- parser .add_argument ("-x" , "--exclude" , type = str , nargs = "*" , help = "Exclude pattern" )
6157parser .add_argument (
62- "-p" , "--python-version" , type = python_version , nargs = "*" , action = "extend" , help = "These versions only (major[.minor])"
58+ "filter" ,
59+ type = valid_path ,
60+ nargs = "*" ,
61+ help = 'Test these files and directories (defaults to all files in the "stdlib" and "stubs" directories)' ,
6362)
63+ parser .add_argument ("-x" , "--exclude" , type = valid_path , nargs = "*" , help = "Exclude these files and directories" )
64+ parser .add_argument ("-v" , "--verbose" , action = "count" , default = 0 , help = "More output" )
6465parser .add_argument (
65- "-d" ,
66- "--dir" ,
67- choices = TYPESHED_DIRECTORIES ,
66+ "-p" ,
67+ "--python-version" ,
68+ type = str ,
69+ choices = SUPPORTED_VERSIONS ,
6870 nargs = "*" ,
6971 action = "extend" ,
70- help = "Test only these top-level typeshed directories (defaults to all typeshed directories )" ,
72+ help = "These versions only (major[.minor] )" ,
7173)
7274parser .add_argument (
7375 "--platform" ,
@@ -76,20 +78,17 @@ class CommandLineArgs(argparse.Namespace):
7678 action = "extend" ,
7779 help = "Run mypy for certain OS platforms (defaults to sys.platform only)" ,
7880)
79- parser .add_argument ("filter" , type = str , nargs = "*" , help = "Include pattern (default all)" )
8081
8182
8283@dataclass
8384class TestConfig :
8485 """Configuration settings for a single run of the `test_typeshed` function."""
8586
8687 verbose : int
87- exclude : list [str ] | None
88- major : MajorVersion
89- minor : MinorVersion
90- directories : frozenset [Directory ]
88+ filter : list [Path ]
89+ exclude : list [Path ]
90+ version : VersionString
9191 platform : Platform
92- filter : list [str ]
9392
9493
9594def log (args : TestConfig , * varargs : object ) -> None :
@@ -98,39 +97,35 @@ def log(args: TestConfig, *varargs: object) -> None:
9897
9998
10099def match (path : Path , args : TestConfig ) -> bool :
101- fn = str (path )
102- if not args .filter and not args .exclude :
103- log (args , fn , "accept by default" )
104- return True
105- if args .exclude :
106- for f in args .exclude :
107- if re .search (f , fn ):
108- log (args , fn , "excluded by pattern" , f )
109- return False
110- if args .filter :
111- for f in args .filter :
112- if re .search (f , fn ):
113- log (args , fn , "accepted by pattern" , f )
114- return True
115- if args .filter :
116- log (args , fn , "rejected (no pattern matches)" )
117- return False
118- log (args , fn , "accepted (no exclude pattern matches)" )
119- return True
120-
121-
122- _VERSION_LINE_RE = re .compile (r"^([a-zA-Z_][a-zA-Z0-9_.]*): ([23]\.\d{1,2})-([23]\.\d{1,2})?$" )
123-
124-
125- def parse_versions (fname : StrPath ) -> dict [str , tuple [MinVersion , MaxVersion ]]:
100+ for excluded_path in args .exclude :
101+ if path == excluded_path :
102+ log (args , path , "explicitly excluded" )
103+ return False
104+ if excluded_path in path .parents :
105+ log (args , path , f'is in an explicitly excluded directory "{ excluded_path } "' )
106+ return False
107+ for included_path in args .filter :
108+ if path == included_path :
109+ log (args , path , "was explicitly included" )
110+ return True
111+ if included_path in path .parents :
112+ log (args , path , f'is in an explicitly included directory "{ included_path } "' )
113+ return True
114+ log_msg = (
115+ f'is implicitly excluded: was not in any of the directories or paths specified on the command line: "{ args .filter !r} "'
116+ )
117+ log (args , path , log_msg )
118+ return False
119+
120+
121+ def parse_versions (fname : StrPath ) -> dict [str , tuple [VersionTuple , VersionTuple ]]:
126122 result = {}
127123 with open (fname ) as f :
128124 for line in f :
129- # Allow having some comments or empty lines.
130- line = line .split ("#" )[0 ].strip ()
125+ line = strip_comments (line )
131126 if line == "" :
132127 continue
133- m = _VERSION_LINE_RE .match (line )
128+ m = VERSION_LINE_RE .match (line )
134129 assert m , f"invalid VERSIONS line: { line } "
135130 mod : str = m .group (1 )
136131 min_version = parse_version (m .group (2 ))
@@ -151,10 +146,11 @@ def parse_version(v_str: str) -> tuple[int, int]:
151146def add_files (files : list [Path ], seen : set [str ], module : Path , args : TestConfig ) -> None :
152147 """Add all files in package or module represented by 'name' located in 'root'."""
153148 if module .is_file () and module .suffix == ".pyi" :
154- files .append (module )
155- seen .add (module .stem )
149+ if match (module , args ):
150+ files .append (module )
151+ seen .add (module .stem )
156152 else :
157- to_add = sorted (module .rglob ("*.pyi" ))
153+ to_add = sorted (file for file in module .rglob ("*.pyi" ) if match ( file , args ))
158154 files .extend (to_add )
159155 seen .update (path .stem for path in to_add )
160156
@@ -239,7 +235,7 @@ def run_mypy(args: TestConfig, configurations: list[MypyDistConf], files: list[P
239235def get_mypy_flags (args : TestConfig , temp_name : str ) -> list [str ]:
240236 return [
241237 "--python-version" ,
242- f" { args .major } . { args . minor } " ,
238+ args .version ,
243239 "--show-traceback" ,
244240 "--warn-incomplete-stub" ,
245241 "--show-error-codes" ,
@@ -323,7 +319,8 @@ def test_stdlib(code: int, args: TestConfig) -> TestResults:
323319 if name == "VERSIONS" or name .startswith ("." ):
324320 continue
325321 module = Path (name ).stem
326- if supported_versions [module ][0 ] <= (args .major , args .minor ) <= supported_versions [module ][1 ]:
322+ module_min_version , module_max_version = supported_versions [module ]
323+ if module_min_version <= tuple (map (int , args .version .split ("." ))) <= module_max_version :
327324 add_files (files , seen , (stdlib / name ), args )
328325
329326 if files :
@@ -354,14 +351,15 @@ def test_third_party_stubs(code: int, args: TestConfig) -> TestResults:
354351
355352
356353def test_typeshed (code : int , args : TestConfig ) -> TestResults :
357- print (f"*** Testing Python { args .major } . { args . minor } on { args .platform } " )
354+ print (f"*** Testing Python { args .version } on { args .platform } " )
358355 files_checked_this_version = 0
359- if "stdlib" in args .directories :
356+ stdlib_dir , stubs_dir = Path ("stdlib" ), Path ("stubs" )
357+ if stdlib_dir in args .filter or any (stdlib_dir in path .parents for path in args .filter ):
360358 code , stdlib_files_checked = test_stdlib (code , args )
361359 files_checked_this_version += stdlib_files_checked
362360 print ()
363361
364- if "stubs" in args .directories :
362+ if stubs_dir in args .filter or any ( stubs_dir in path . parents for path in args . filter ) :
365363 code , third_party_files_checked = test_third_party_stubs (code , args )
366364 files_checked_this_version += third_party_files_checked
367365 print ()
@@ -373,19 +371,12 @@ def main() -> None:
373371 args = parser .parse_args (namespace = CommandLineArgs ())
374372 versions = args .python_version or SUPPORTED_VERSIONS
375373 platforms = args .platform or [sys .platform ]
376- tested_directories = frozenset (args .dir ) if args .dir else TYPESHED_DIRECTORIES
374+ filter = args .filter or DIRECTORIES_TO_TEST
375+ exclude = args .exclude or []
377376 code = 0
378377 total_files_checked = 0
379- for (major , minor ), platform in product (versions , platforms ):
380- config = TestConfig (
381- verbose = args .verbose ,
382- exclude = args .exclude ,
383- major = major ,
384- minor = minor ,
385- directories = tested_directories ,
386- platform = platform ,
387- filter = args .filter ,
388- )
378+ for version , platform in product (versions , platforms ):
379+ config = TestConfig (args .verbose , filter , exclude , version , platform )
389380 code , files_checked_this_version = test_typeshed (code , args = config )
390381 total_files_checked += files_checked_this_version
391382 if code :
0 commit comments