Skip to content

Commit ca484c4

Browse files
committed
Share venvs between packages with common requirements
1 parent ee036e7 commit ca484c4

File tree

1 file changed

+56
-32
lines changed

1 file changed

+56
-32
lines changed

tests/mypy_test.py

Lines changed: 56 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import subprocess
1111
import sys
1212
import tempfile
13+
import time
14+
from collections import defaultdict
1315
from dataclasses import dataclass
1416
from itertools import product
1517
from 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

390391
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():
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

420443
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
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

Comments
 (0)