1010import subprocess
1111import sys
1212import tempfile
13+ import time
14+ from collections import defaultdict
1315from dataclasses import dataclass
1416from itertools import product
1517from pathlib import Path
@@ -246,7 +248,7 @@ def run_mypy(
246248 # Stub completion is checked by pyright (--allow-*-defs)
247249 "--allow-untyped-defs" ,
248250 "--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?
250252 "--enable-error-code" ,
251253 "ignore-without-code" ,
252254 "--config-file" ,
@@ -364,22 +366,21 @@ def test_stdlib(code: int, args: TestConfig) -> TestResults:
364366
365367
366368_PRINT_LOCK = Lock ()
367- _PYTHON_EXE_MAPPING : dict [str , VenvInfo ] = {}
369+ _DISTRIBUTION_TO_VENV_MAPPING : dict [str , VenvInfo ] = {}
368370
369371
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 )
373376
374377
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 :
378379 # 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" ]
380381 if args .verbose :
381382 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" ))
383384 try :
384385 subprocess .run (pip_command , check = True , capture_output = True , text = True )
385386 except subprocess .CalledProcessError as e :
@@ -388,33 +389,55 @@ def install_requirements_for_distribution(
388389
389390
390391def 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 ():
393397 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 )
395400 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 ] = {}
397404
398405 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 ()
400410
401411 with concurrent .futures .ThreadPoolExecutor () as executor :
402412 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
404415 ]
405416 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
408419
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 ))
418441
419442
420443def 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
436459 ):
437460 distributions_to_check [distribution ] = get_recursive_requirements (distribution )
438461
439- if not _PYTHON_EXE_MAPPING :
462+ if not _DISTRIBUTION_TO_VENV_MAPPING :
440463 setup_virtual_environments (distributions_to_check , args , tempdir )
441464
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
445469 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 )
447471 )
448472 code = max (code , this_code )
449473 files_checked += checked
0 commit comments