55
66import argparse
77import concurrent .futures
8- import functools
98import os
109import subprocess
1110import sys
1211import tempfile
1312import time
1413from collections import defaultdict
15- from collections .abc import Generator
1614from dataclasses import dataclass
1715from enum import Enum
1816from itertools import product
2119from typing import Annotated , Any , NamedTuple
2220from typing_extensions import TypeAlias
2321
24- import tomli
2522from packaging .requirements import Requirement
2623
27- from ts_utils .metadata import PackageDependencies , get_recursive_requirements , metadata_path , read_metadata
24+ from ts_utils .metadata import PackageDependencies , get_recursive_requirements , read_metadata
25+ from ts_utils .mypy import MypyDistConf , mypy_configuration_from_distribution , temporary_mypy_config_file
2826from ts_utils .paths import STDLIB_PATH , STUBS_PATH , TESTS_DIR , TS_BASE_PATH , distribution_path
2927from ts_utils .utils import (
3028 PYTHON_VERSION ,
4644 print_error ("Cannot import mypy. Did you install it?" )
4745 sys .exit (1 )
4846
49- # We need to work around a limitation of tempfile.NamedTemporaryFile on Windows
50- # For details, see https://github.com/python/typeshed/pull/13620#discussion_r1990185997
51- # Python 3.12 added a workaround with `tempfile.NamedTemporaryFile("w+", delete_on_close=False)`
52- if sys .platform != "win32" :
53- _named_temporary_file = functools .partial (tempfile .NamedTemporaryFile , "w+" )
54- else :
55- from contextlib import contextmanager
56-
57- @contextmanager
58- def _named_temporary_file () -> Generator [tempfile ._TemporaryFileWrapper [str ]]: # pyright: ignore[reportPrivateUsage]
59- temp = tempfile .NamedTemporaryFile ("w+" , delete = False ) # noqa: SIM115
60- try :
61- yield temp
62- finally :
63- temp .close ()
64- os .remove (temp .name )
65-
66-
6747SUPPORTED_VERSIONS = ["3.13" , "3.12" , "3.11" , "3.10" , "3.9" ]
6848SUPPORTED_PLATFORMS = ("linux" , "win32" , "darwin" )
6949DIRECTORIES_TO_TEST = [STDLIB_PATH , STUBS_PATH ]
@@ -177,49 +157,20 @@ def add_files(files: list[Path], module: Path, args: TestConfig) -> None:
177157 files .extend (sorted (file for file in module .rglob ("*.pyi" ) if match (file , args )))
178158
179159
180- class MypyDistConf (NamedTuple ):
181- module_name : str
182- values : dict [str , dict [str , Any ]]
183-
184-
185- # The configuration section in the metadata file looks like the following, with multiple module sections possible
186- # [mypy-tests]
187- # [mypy-tests.yaml]
188- # module_name = "yaml"
189- # [mypy-tests.yaml.values]
190- # disallow_incomplete_defs = true
191- # disallow_untyped_defs = true
192-
193-
194- def add_configuration (configurations : list [MypyDistConf ], distribution : str ) -> None :
195- with metadata_path (distribution ).open ("rb" ) as f :
196- data = tomli .load (f )
197-
198- # TODO: This could be added to ts_utils.metadata, but is currently unused
199- mypy_tests_conf : dict [str , dict [str , Any ]] = data .get ("mypy-tests" , {})
200- if not mypy_tests_conf :
201- return
202-
203- assert isinstance (mypy_tests_conf , dict ), "mypy-tests should be a section"
204- for section_name , mypy_section in mypy_tests_conf .items ():
205- assert isinstance (mypy_section , dict ), f"{ section_name } should be a section"
206- module_name = mypy_section .get ("module_name" )
207-
208- assert module_name is not None , f"{ section_name } should have a module_name key"
209- assert isinstance (module_name , str ), f"{ section_name } should be a key-value pair"
210-
211- assert "values" in mypy_section , f"{ section_name } should have a values section"
212- values : dict [str , dict [str , Any ]] = mypy_section ["values" ]
213- assert isinstance (values , dict ), "values should be a section"
214-
215- configurations .append (MypyDistConf (module_name , values .copy ()))
216-
217-
218160class MypyResult (Enum ):
219161 SUCCESS = 0
220162 FAILURE = 1
221163 CRASH = 2
222164
165+ @staticmethod
166+ def from_process_result (result : subprocess .CompletedProcess [Any ]) -> MypyResult :
167+ if result .returncode == 0 :
168+ return MypyResult .SUCCESS
169+ elif result .returncode == 1 :
170+ return MypyResult .FAILURE
171+ else :
172+ return MypyResult .CRASH
173+
223174
224175def run_mypy (
225176 args : TestConfig ,
@@ -234,15 +185,7 @@ def run_mypy(
234185 env_vars = dict (os .environ )
235186 if mypypath is not None :
236187 env_vars ["MYPYPATH" ] = mypypath
237-
238- with _named_temporary_file () as temp :
239- temp .write ("[mypy]\n " )
240- for dist_conf in configurations :
241- temp .write (f"[mypy-{ dist_conf .module_name } ]\n " )
242- for k , v in dist_conf .values .items ():
243- temp .write (f"{ k } = { v } \n " )
244- temp .flush ()
245-
188+ with temporary_mypy_config_file (configurations ) as temp :
246189 flags = [
247190 "--python-version" ,
248191 args .version ,
@@ -278,29 +221,23 @@ def run_mypy(
278221 if args .verbose :
279222 print (colored (f"running { ' ' .join (mypy_command )} " , "blue" ))
280223 result = subprocess .run (mypy_command , capture_output = True , text = True , env = env_vars , check = False )
281- if result .returncode :
282- print_error (f"failure (exit code { result .returncode } )\n " )
283- if result .stdout :
284- print_error (result .stdout )
285- if result .stderr :
286- print_error (result .stderr )
287- if non_types_dependencies and args .verbose :
288- print ("Ran with the following environment:" )
289- subprocess .run (["uv" , "pip" , "freeze" ], env = {** os .environ , "VIRTUAL_ENV" : str (venv_dir )}, check = False )
290- print ()
291- else :
292- print_success_msg ()
293- if result .returncode == 0 :
294- return MypyResult .SUCCESS
295- elif result .returncode == 1 :
296- return MypyResult .FAILURE
297- else :
298- return MypyResult .CRASH
224+ if result .returncode :
225+ print_error (f"failure (exit code { result .returncode } )\n " )
226+ if result .stdout :
227+ print_error (result .stdout )
228+ if result .stderr :
229+ print_error (result .stderr )
230+ if non_types_dependencies and args .verbose :
231+ print ("Ran with the following environment:" )
232+ subprocess .run (["uv" , "pip" , "freeze" ], env = {** os .environ , "VIRTUAL_ENV" : str (venv_dir )}, check = False )
233+ print ()
234+ else :
235+ print_success_msg ()
236+
237+ return MypyResult .from_process_result (result )
299238
300239
301- def add_third_party_files (
302- distribution : str , files : list [Path ], args : TestConfig , configurations : list [MypyDistConf ], seen_dists : set [str ]
303- ) -> None :
240+ def add_third_party_files (distribution : str , files : list [Path ], args : TestConfig , seen_dists : set [str ]) -> None :
304241 typeshed_reqs = get_recursive_requirements (distribution ).typeshed_pkgs
305242 if distribution in seen_dists :
306243 return
@@ -311,7 +248,6 @@ def add_third_party_files(
311248 if name .startswith ("." ):
312249 continue
313250 add_files (files , (root / name ), args )
314- add_configuration (configurations , distribution )
315251
316252
317253class TestResult (NamedTuple ):
@@ -328,9 +264,9 @@ def test_third_party_distribution(
328264 and the second element is the number of checked files.
329265 """
330266 files : list [Path ] = []
331- configurations : list [MypyDistConf ] = []
332267 seen_dists : set [str ] = set ()
333- add_third_party_files (distribution , files , args , configurations , seen_dists )
268+ add_third_party_files (distribution , files , args , seen_dists )
269+ configurations = mypy_configuration_from_distribution (distribution )
334270
335271 if not files and args .filter :
336272 return TestResult (MypyResult .SUCCESS , 0 )
0 commit comments