10
10
import subprocess
11
11
import sys
12
12
import tempfile
13
+ import time
14
+ from collections import defaultdict
13
15
from dataclasses import dataclass
14
16
from itertools import product
15
17
from pathlib import Path
@@ -246,7 +248,7 @@ def run_mypy(
246
248
# Stub completion is checked by pyright (--allow-*-defs)
247
249
"--allow-untyped-defs" ,
248
250
"--allow-incomplete-defs" ,
249
- "--allow-subclassing-any" , # Needed until we can use non-types dependencies #5768
251
+ "--allow-subclassing-any" , # TODO: Do we still need this now that non-types dependencies are allowed?
250
252
"--enable-error-code" ,
251
253
"ignore-without-code" ,
252
254
"--config-file" ,
@@ -364,22 +366,21 @@ def test_stdlib(code: int, args: TestConfig) -> TestResults:
364
366
365
367
366
368
_PRINT_LOCK = Lock ()
367
- _PYTHON_EXE_MAPPING : dict [str , VenvInfo ] = {}
369
+ _DISTRIBUTION_TO_VENV_MAPPING : dict [str , VenvInfo ] = {}
368
370
369
371
370
- def setup_venv_for_distribution (distribution : str , tempdir : Path ) -> tuple [str , VenvInfo ]:
371
- venv_dir = tempdir / f".venv-{ distribution } "
372
- return distribution , make_venv (venv_dir )
372
+ def setup_venv_for_external_requirements_set (requirements_set : frozenset [str ], tempdir : Path ) -> tuple [frozenset [str ], VenvInfo ]:
373
+ reqs_joined = "-" .join (sorted (requirements_set ))
374
+ venv_dir = tempdir / f".venv-{ reqs_joined } "
375
+ return requirements_set , make_venv (venv_dir )
373
376
374
377
375
- def install_requirements_for_distribution (
376
- distribution : str , pip_exe : str , args : TestConfig , external_requirements : tuple [str , ...]
377
- ) -> None :
378
+ def install_requirements_for_venv (venv_info : VenvInfo , args : TestConfig , external_requirements : frozenset [str ]) -> None :
378
379
# Use --no-cache-dir to avoid issues with concurrent read/writes to the cache
379
- pip_command = [pip_exe , "install" , get_mypy_req (), * external_requirements , "--no-cache-dir" ]
380
+ pip_command = [venv_info . pip_exe , "install" , get_mypy_req (), * sorted ( external_requirements ) , "--no-cache-dir" ]
380
381
if args .verbose :
381
382
with _PRINT_LOCK :
382
- print (colored (f"pip installing the following requirements for { distribution !r } : { external_requirements } " , "blue" ))
383
+ print (colored (f"Running { pip_command } " , "blue" ))
383
384
try :
384
385
subprocess .run (pip_command , check = True , capture_output = True , text = True )
385
386
except subprocess .CalledProcessError as e :
@@ -388,33 +389,55 @@ def install_requirements_for_distribution(
388
389
389
390
390
391
def setup_virtual_environments (distributions : dict [str , PackageDependencies ], args : TestConfig , tempdir : Path ) -> None :
391
- distributions_needing_venvs : dict [str , PackageDependencies ] = {}
392
- for distribution , requirements in distributions .items ():
392
+ no_external_dependencies_venv = VenvInfo (pip_exe = "" , python_exe = sys .executable )
393
+ external_requirements_to_distributions : defaultdict [frozenset [str ], list [str ]] = defaultdict (list )
394
+ num_pkgs_with_external_reqs = 0
395
+
396
+ for distribution_name , requirements in distributions .items ():
393
397
if requirements .external_pkgs :
394
- distributions_needing_venvs [distribution ] = requirements
398
+ num_pkgs_with_external_reqs += 1
399
+ external_requirements_to_distributions [frozenset (requirements .external_pkgs )].append (distribution_name )
395
400
else :
396
- _PYTHON_EXE_MAPPING [distribution ] = VenvInfo (pip_exe = "" , python_exe = sys .executable )
401
+ _DISTRIBUTION_TO_VENV_MAPPING [distribution_name ] = no_external_dependencies_venv
402
+
403
+ requirements_sets_to_venvs : dict [frozenset [str ], VenvInfo ] = {}
397
404
398
405
if args .verbose :
399
- print (colored (f"Setting up venvs for { list (distributions_needing_venvs )} ..." , "blue" ))
406
+ num_venvs = len (external_requirements_to_distributions )
407
+ msg = f"Setting up { num_venvs } venvs for { num_pkgs_with_external_reqs } distributions... "
408
+ print (colored (msg , "blue" ), end = "" , flush = True )
409
+ venv_start_time = time .perf_counter ()
400
410
401
411
with concurrent .futures .ThreadPoolExecutor () as executor :
402
412
venv_info_futures = [
403
- executor .submit (setup_venv_for_distribution , distribution , tempdir ) for distribution in distributions_needing_venvs
413
+ executor .submit (setup_venv_for_external_requirements_set , requirements_set , tempdir )
414
+ for requirements_set in external_requirements_to_distributions
404
415
]
405
416
for venv_info_future in concurrent .futures .as_completed (venv_info_futures ):
406
- distribution , venv_info = venv_info_future .result ()
407
- _PYTHON_EXE_MAPPING [ distribution ] = venv_info
417
+ requirements_set , venv_info = venv_info_future .result ()
418
+ requirements_sets_to_venvs [ requirements_set ] = venv_info
408
419
409
- # Limit workers to 5 at a time, since this makes network requests
410
- with concurrent .futures .ThreadPoolExecutor (max_workers = 5 ) as executor :
411
- futures = []
412
- for distribution , requirements in distributions_needing_venvs .items ():
413
- pip_exe = _PYTHON_EXE_MAPPING [distribution ].pip_exe
414
- futures .append (
415
- executor .submit (install_requirements_for_distribution , distribution , pip_exe , args , requirements .external_pkgs )
416
- )
417
- concurrent .futures .wait (futures )
420
+ if args .verbose :
421
+ venv_elapsed_time = time .perf_counter () - venv_start_time
422
+ print (colored (f"took { venv_elapsed_time :.2f} seconds" , "blue" ))
423
+ pip_start_time = time .perf_counter ()
424
+
425
+ # Limit workers to 10 at a time, since this makes network requests
426
+ with concurrent .futures .ThreadPoolExecutor (max_workers = 10 ) as executor :
427
+ pip_install_futures = [
428
+ executor .submit (install_requirements_for_venv , venv_info , args , requirements_set )
429
+ for requirements_set , venv_info in requirements_sets_to_venvs .items ()
430
+ ]
431
+ concurrent .futures .wait (pip_install_futures )
432
+
433
+ if args .verbose :
434
+ pip_elapsed_time = time .perf_counter () - pip_start_time
435
+ msg = f"Combined time for installing requirements across all venvs: { pip_elapsed_time :.2f} seconds"
436
+ print (colored (msg , "blue" ))
437
+
438
+ for requirements_set , distribution_list in external_requirements_to_distributions .items ():
439
+ venv_to_use = requirements_sets_to_venvs [requirements_set ]
440
+ _DISTRIBUTION_TO_VENV_MAPPING .update (dict .fromkeys (distribution_list , venv_to_use ))
418
441
419
442
420
443
def test_third_party_stubs (code : int , args : TestConfig , tempdir : Path ) -> TestResults :
@@ -436,14 +459,15 @@ def test_third_party_stubs(code: int, args: TestConfig, tempdir: Path) -> TestRe
436
459
):
437
460
distributions_to_check [distribution ] = get_recursive_requirements (distribution )
438
461
439
- if not _PYTHON_EXE_MAPPING :
462
+ if not _DISTRIBUTION_TO_VENV_MAPPING :
440
463
setup_virtual_environments (distributions_to_check , args , tempdir )
441
464
442
- for distribution , requirements in distributions_to_check .items ():
443
- has_non_types_dependencies = bool (requirements .external_pkgs )
444
- python_to_use = _PYTHON_EXE_MAPPING [distribution ].python_exe
465
+ assert len (_DISTRIBUTION_TO_VENV_MAPPING ) == len (distributions_to_check )
466
+
467
+ for distribution , venv_info in _DISTRIBUTION_TO_VENV_MAPPING .items ():
468
+ venv_python = venv_info .python_exe
445
469
this_code , checked = test_third_party_distribution (
446
- distribution , args , python_exe = python_to_use , non_types_dependencies = has_non_types_dependencies
470
+ distribution , args , python_exe = venv_python , non_types_dependencies = ( venv_python != sys . executable )
447
471
)
448
472
code = max (code , this_code )
449
473
files_checked += checked
0 commit comments