5
5
6
6
import argparse
7
7
import concurrent .futures
8
- import functools
9
8
import os
10
9
import subprocess
11
10
import sys
12
11
import tempfile
13
12
import time
14
13
from collections import defaultdict
15
- from collections .abc import Generator
16
14
from dataclasses import dataclass
17
15
from enum import Enum
18
16
from itertools import product
21
19
from typing import Annotated , Any , NamedTuple
22
20
from typing_extensions import TypeAlias
23
21
24
- import tomli
25
22
from packaging .requirements import Requirement
26
23
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
28
26
from ts_utils .paths import STDLIB_PATH , STUBS_PATH , TESTS_DIR , TS_BASE_PATH , distribution_path
29
27
from ts_utils .utils import (
30
28
PYTHON_VERSION ,
46
44
print_error ("Cannot import mypy. Did you install it?" )
47
45
sys .exit (1 )
48
46
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
-
67
47
SUPPORTED_VERSIONS = ["3.13" , "3.12" , "3.11" , "3.10" , "3.9" ]
68
48
SUPPORTED_PLATFORMS = ("linux" , "win32" , "darwin" )
69
49
DIRECTORIES_TO_TEST = [STDLIB_PATH , STUBS_PATH ]
@@ -177,49 +157,20 @@ def add_files(files: list[Path], module: Path, args: TestConfig) -> None:
177
157
files .extend (sorted (file for file in module .rglob ("*.pyi" ) if match (file , args )))
178
158
179
159
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
-
218
160
class MypyResult (Enum ):
219
161
SUCCESS = 0
220
162
FAILURE = 1
221
163
CRASH = 2
222
164
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
+
223
174
224
175
def run_mypy (
225
176
args : TestConfig ,
@@ -234,15 +185,7 @@ def run_mypy(
234
185
env_vars = dict (os .environ )
235
186
if mypypath is not None :
236
187
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 :
246
189
flags = [
247
190
"--python-version" ,
248
191
args .version ,
@@ -278,29 +221,23 @@ def run_mypy(
278
221
if args .verbose :
279
222
print (colored (f"running { ' ' .join (mypy_command )} " , "blue" ))
280
223
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 )
299
238
300
239
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 :
304
241
typeshed_reqs = get_recursive_requirements (distribution ).typeshed_pkgs
305
242
if distribution in seen_dists :
306
243
return
@@ -311,7 +248,6 @@ def add_third_party_files(
311
248
if name .startswith ("." ):
312
249
continue
313
250
add_files (files , (root / name ), args )
314
- add_configuration (configurations , distribution )
315
251
316
252
317
253
class TestResult (NamedTuple ):
@@ -328,9 +264,9 @@ def test_third_party_distribution(
328
264
and the second element is the number of checked files.
329
265
"""
330
266
files : list [Path ] = []
331
- configurations : list [MypyDistConf ] = []
332
267
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 )
334
270
335
271
if not files and args .filter :
336
272
return TestResult (MypyResult .SUCCESS , 0 )
0 commit comments