mirror of
https://github.com/davidhalter/typeshed.git
synced 2025-12-16 00:37:10 +08:00
Use uv for installing dynamic dependencies in mypy_test.py and regr_test.py (#11517)
This commit is contained in:
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -141,7 +141,7 @@ jobs:
|
||||
if [ -n "$DEPENDENCIES" ]; then
|
||||
source .venv/bin/activate
|
||||
echo "Installing packages: $DEPENDENCIES"
|
||||
uv pip install $DEPENDENCIES --system
|
||||
uv pip install $DEPENDENCIES
|
||||
fi
|
||||
- name: Activate the isolated venv for the rest of the job
|
||||
run: echo "$PWD/.venv/bin" >> $GITHUB_PATH
|
||||
|
||||
@@ -20,6 +20,7 @@ termcolor>=2.3
|
||||
tomli==2.0.1
|
||||
tomlkit==0.12.3
|
||||
typing_extensions>=4.9.0rc1
|
||||
uv
|
||||
|
||||
# Type stubs used to type check our scripts.
|
||||
types-pyyaml>=6.0.12.7
|
||||
|
||||
@@ -30,15 +30,14 @@ from parse_metadata import PackageDependencies, get_recursive_requirements, read
|
||||
from utils import (
|
||||
PYTHON_VERSION,
|
||||
VERSIONS_RE as VERSION_LINE_RE,
|
||||
VenvInfo,
|
||||
colored,
|
||||
get_gitignore_spec,
|
||||
get_mypy_req,
|
||||
make_venv,
|
||||
print_error,
|
||||
print_success_msg,
|
||||
spec_matches_path,
|
||||
strip_comments,
|
||||
venv_python,
|
||||
)
|
||||
|
||||
# Fail early if mypy isn't installed
|
||||
@@ -235,7 +234,7 @@ def run_mypy(
|
||||
*,
|
||||
testing_stdlib: bool,
|
||||
non_types_dependencies: bool,
|
||||
venv_info: VenvInfo,
|
||||
venv_dir: Path | None,
|
||||
mypypath: str | None = None,
|
||||
) -> MypyResult:
|
||||
env_vars = dict(os.environ)
|
||||
@@ -279,7 +278,8 @@ def run_mypy(
|
||||
flags.append("--no-site-packages")
|
||||
|
||||
mypy_args = [*flags, *map(str, files)]
|
||||
mypy_command = [venv_info.python_exe, "-m", "mypy", *mypy_args]
|
||||
python_path = sys.executable if venv_dir is None else str(venv_python(venv_dir))
|
||||
mypy_command = [python_path, "-m", "mypy", *mypy_args]
|
||||
if args.verbose:
|
||||
print(colored(f"running {' '.join(mypy_command)}", "blue"))
|
||||
result = subprocess.run(mypy_command, capture_output=True, text=True, env=env_vars)
|
||||
@@ -291,7 +291,7 @@ def run_mypy(
|
||||
print_error(result.stderr)
|
||||
if non_types_dependencies and args.verbose:
|
||||
print("Ran with the following environment:")
|
||||
subprocess.run([venv_info.pip_exe, "freeze", "--all"])
|
||||
subprocess.run(["uv", "pip", "freeze"], env={**os.environ, "VIRTUAL_ENV": str(venv_dir)})
|
||||
print()
|
||||
else:
|
||||
print_success_msg()
|
||||
@@ -324,7 +324,7 @@ class TestResult(NamedTuple):
|
||||
|
||||
|
||||
def test_third_party_distribution(
|
||||
distribution: str, args: TestConfig, venv_info: VenvInfo, *, non_types_dependencies: bool
|
||||
distribution: str, args: TestConfig, venv_dir: Path | None, *, non_types_dependencies: bool
|
||||
) -> TestResult:
|
||||
"""Test the stubs of a third-party distribution.
|
||||
|
||||
@@ -353,7 +353,7 @@ def test_third_party_distribution(
|
||||
args,
|
||||
configurations,
|
||||
files,
|
||||
venv_info=venv_info,
|
||||
venv_dir=venv_dir,
|
||||
mypypath=mypypath,
|
||||
testing_stdlib=False,
|
||||
non_types_dependencies=non_types_dependencies,
|
||||
@@ -377,9 +377,8 @@ def test_stdlib(args: TestConfig) -> TestResult:
|
||||
return TestResult(MypyResult.SUCCESS, 0)
|
||||
|
||||
print(f"Testing stdlib ({len(files)} files)... ", end="", flush=True)
|
||||
# We don't actually need pip for the stdlib testing
|
||||
venv_info = VenvInfo(pip_exe="", python_exe=sys.executable)
|
||||
result = run_mypy(args, [], files, venv_info=venv_info, testing_stdlib=True, non_types_dependencies=False)
|
||||
# We don't actually need to install anything for the stdlib testing
|
||||
result = run_mypy(args, [], files, venv_dir=None, testing_stdlib=True, non_types_dependencies=False)
|
||||
return TestResult(result, len(files))
|
||||
|
||||
|
||||
@@ -409,22 +408,30 @@ class TestSummary:
|
||||
|
||||
|
||||
_PRINT_LOCK = Lock()
|
||||
_DISTRIBUTION_TO_VENV_MAPPING: dict[str, VenvInfo] = {}
|
||||
_DISTRIBUTION_TO_VENV_MAPPING: dict[str, Path | None] = {}
|
||||
|
||||
|
||||
def setup_venv_for_external_requirements_set(requirements_set: frozenset[str], tempdir: Path) -> tuple[frozenset[str], VenvInfo]:
|
||||
def setup_venv_for_external_requirements_set(
|
||||
requirements_set: frozenset[str], tempdir: Path, args: TestConfig
|
||||
) -> tuple[frozenset[str], Path]:
|
||||
venv_dir = tempdir / f".venv-{hash(requirements_set)}"
|
||||
return requirements_set, make_venv(venv_dir)
|
||||
uv_command = ["uv", "venv", str(venv_dir)]
|
||||
if not args.verbose:
|
||||
uv_command.append("--quiet")
|
||||
subprocess.run(uv_command, check=True)
|
||||
return requirements_set, venv_dir
|
||||
|
||||
|
||||
def install_requirements_for_venv(venv_info: VenvInfo, args: TestConfig, external_requirements: frozenset[str]) -> None:
|
||||
def install_requirements_for_venv(venv_dir: Path, args: TestConfig, external_requirements: frozenset[str]) -> None:
|
||||
# Use --no-cache-dir to avoid issues with concurrent read/writes to the cache
|
||||
pip_command = [venv_info.pip_exe, "install", get_mypy_req(), *sorted(external_requirements), "--no-cache-dir"]
|
||||
uv_command = ["uv", "pip", "install", get_mypy_req(), *sorted(external_requirements), "--no-cache-dir"]
|
||||
if args.verbose:
|
||||
with _PRINT_LOCK:
|
||||
print(colored(f"Running {pip_command}", "blue"))
|
||||
print(colored(f"Running {uv_command}", "blue"))
|
||||
else:
|
||||
uv_command.append("--quiet")
|
||||
try:
|
||||
subprocess.run(pip_command, check=True, capture_output=True, text=True)
|
||||
subprocess.run(uv_command, check=True, text=True, env={**os.environ, "VIRTUAL_ENV": str(venv_dir)})
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(e.stderr)
|
||||
raise
|
||||
@@ -437,9 +444,6 @@ def setup_virtual_environments(distributions: dict[str, PackageDependencies], ar
|
||||
|
||||
# STAGE 1: Determine which (if any) stubs packages require virtual environments.
|
||||
# Group stubs packages according to their external-requirements sets
|
||||
|
||||
# We don't actually need pip if there aren't any external dependencies
|
||||
no_external_dependencies_venv = VenvInfo(pip_exe="", python_exe=sys.executable)
|
||||
external_requirements_to_distributions: defaultdict[frozenset[str], list[str]] = defaultdict(list)
|
||||
num_pkgs_with_external_reqs = 0
|
||||
|
||||
@@ -449,7 +453,7 @@ def setup_virtual_environments(distributions: dict[str, PackageDependencies], ar
|
||||
external_requirements = frozenset(requirements.external_pkgs)
|
||||
external_requirements_to_distributions[external_requirements].append(distribution_name)
|
||||
else:
|
||||
_DISTRIBUTION_TO_VENV_MAPPING[distribution_name] = no_external_dependencies_venv
|
||||
_DISTRIBUTION_TO_VENV_MAPPING[distribution_name] = None
|
||||
|
||||
# Exit early if there are no stubs packages that have non-types dependencies
|
||||
if num_pkgs_with_external_reqs == 0:
|
||||
@@ -458,7 +462,7 @@ def setup_virtual_environments(distributions: dict[str, PackageDependencies], ar
|
||||
return
|
||||
|
||||
# STAGE 2: Setup a virtual environment for each unique set of external requirements
|
||||
requirements_sets_to_venvs: dict[frozenset[str], VenvInfo] = {}
|
||||
requirements_sets_to_venvs: dict[frozenset[str], Path] = {}
|
||||
|
||||
if args.verbose:
|
||||
num_venvs = len(external_requirements_to_distributions)
|
||||
@@ -472,13 +476,13 @@ def setup_virtual_environments(distributions: dict[str, PackageDependencies], ar
|
||||
venv_start_time = time.perf_counter()
|
||||
|
||||
with concurrent.futures.ProcessPoolExecutor() as executor:
|
||||
venv_info_futures = [
|
||||
executor.submit(setup_venv_for_external_requirements_set, requirements_set, tempdir)
|
||||
venv_futures = [
|
||||
executor.submit(setup_venv_for_external_requirements_set, requirements_set, tempdir, args)
|
||||
for requirements_set in external_requirements_to_distributions
|
||||
]
|
||||
for venv_info_future in concurrent.futures.as_completed(venv_info_futures):
|
||||
requirements_set, venv_info = venv_info_future.result()
|
||||
requirements_sets_to_venvs[requirements_set] = venv_info
|
||||
for venv_future in concurrent.futures.as_completed(venv_futures):
|
||||
requirements_set, venv_dir = venv_future.result()
|
||||
requirements_sets_to_venvs[requirements_set] = venv_dir
|
||||
|
||||
venv_elapsed_time = time.perf_counter() - venv_start_time
|
||||
|
||||
@@ -492,8 +496,8 @@ def setup_virtual_environments(distributions: dict[str, PackageDependencies], ar
|
||||
# Limit workers to 10 at a time, since this makes network requests
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
|
||||
pip_install_futures = [
|
||||
executor.submit(install_requirements_for_venv, venv_info, args, requirements_set)
|
||||
for requirements_set, venv_info in requirements_sets_to_venvs.items()
|
||||
executor.submit(install_requirements_for_venv, venv_dir, args, requirements_set)
|
||||
for requirements_set, venv_dir in requirements_sets_to_venvs.items()
|
||||
]
|
||||
concurrent.futures.wait(pip_install_futures)
|
||||
|
||||
@@ -561,10 +565,10 @@ def test_third_party_stubs(args: TestConfig, tempdir: Path) -> TestSummary:
|
||||
assert _DISTRIBUTION_TO_VENV_MAPPING.keys() >= distributions_to_check.keys()
|
||||
|
||||
for distribution in distributions_to_check:
|
||||
venv_info = _DISTRIBUTION_TO_VENV_MAPPING[distribution]
|
||||
non_types_dependencies = venv_info.python_exe != sys.executable
|
||||
venv_dir = _DISTRIBUTION_TO_VENV_MAPPING[distribution]
|
||||
non_types_dependencies = venv_dir is not None
|
||||
mypy_result, files_checked = test_third_party_distribution(
|
||||
distribution, args, venv_info=venv_info, non_types_dependencies=non_types_dependencies
|
||||
distribution, args, venv_dir=venv_dir, non_types_dependencies=non_types_dependencies
|
||||
)
|
||||
summary.register_result(mypy_result, files_checked)
|
||||
|
||||
|
||||
@@ -25,13 +25,12 @@ from parse_metadata import get_recursive_requirements, read_metadata
|
||||
from utils import (
|
||||
PYTHON_VERSION,
|
||||
PackageInfo,
|
||||
VenvInfo,
|
||||
colored,
|
||||
get_all_testcase_directories,
|
||||
get_mypy_req,
|
||||
make_venv,
|
||||
print_error,
|
||||
testcase_dir_from_package_name,
|
||||
venv_python,
|
||||
)
|
||||
|
||||
ReturnCode: TypeAlias = int
|
||||
@@ -148,13 +147,16 @@ def setup_testcase_dir(package: PackageInfo, tempdir: Path, verbosity: Verbosity
|
||||
shutil.copytree(Path("stubs", requirement), new_typeshed / "stubs" / requirement)
|
||||
|
||||
if requirements.external_pkgs:
|
||||
pip_exe = make_venv(tempdir / VENV_DIR).pip_exe
|
||||
venv_location = str(tempdir / VENV_DIR)
|
||||
subprocess.run(["uv", "venv", venv_location], check=True, capture_output=True)
|
||||
# Use --no-cache-dir to avoid issues with concurrent read/writes to the cache
|
||||
pip_command = [pip_exe, "install", get_mypy_req(), *requirements.external_pkgs, "--no-cache-dir"]
|
||||
uv_command = ["uv", "pip", "install", get_mypy_req(), *requirements.external_pkgs, "--no-cache-dir"]
|
||||
if verbosity is Verbosity.VERBOSE:
|
||||
verbose_log(f"{package.name}: Setting up venv in {tempdir / VENV_DIR}. {pip_command=}\n")
|
||||
verbose_log(f"{package.name}: Setting up venv in {venv_location}. {uv_command=}\n")
|
||||
try:
|
||||
subprocess.run(pip_command, check=True, capture_output=True, text=True)
|
||||
subprocess.run(
|
||||
uv_command, check=True, capture_output=True, text=True, env=os.environ | {"VIRTUAL_ENV": venv_location}
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
_PRINT_QUEUE.put(f"{package.name}\n{e.stderr}")
|
||||
raise
|
||||
@@ -193,7 +195,7 @@ def run_testcases(
|
||||
env_vars["MYPYPATH"] = os.pathsep.join(map(str, custom_typeshed.glob("stubs/*")))
|
||||
has_non_types_dependencies = (tempdir / VENV_DIR).exists()
|
||||
if has_non_types_dependencies:
|
||||
python_exe = VenvInfo.of_existing_venv(tempdir / VENV_DIR).python_exe
|
||||
python_exe = str(venv_python(tempdir / VENV_DIR))
|
||||
else:
|
||||
python_exe = sys.executable
|
||||
flags.append("--no-site-packages")
|
||||
|
||||
@@ -13,7 +13,7 @@ from textwrap import dedent
|
||||
from typing import NoReturn
|
||||
|
||||
from parse_metadata import NoSuchStubError, get_recursive_requirements, read_metadata
|
||||
from utils import PYTHON_VERSION, colored, get_mypy_req, make_venv, print_error, print_success_msg
|
||||
from utils import PYTHON_VERSION, colored, get_mypy_req, print_error, print_success_msg
|
||||
|
||||
|
||||
def run_stubtest(
|
||||
@@ -43,11 +43,13 @@ def run_stubtest(
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
venv_dir = Path(tmp)
|
||||
try:
|
||||
pip_exe, python_exe = make_venv(venv_dir)
|
||||
except Exception:
|
||||
print_error("fail")
|
||||
raise
|
||||
subprocess.run(["uv", "venv", venv_dir, "--seed"], capture_output=True, check=True)
|
||||
if sys.platform == "win32":
|
||||
pip_exe = str(venv_dir / "Scripts" / "pip.exe")
|
||||
python_exe = str(venv_dir / "Scripts" / "python.exe")
|
||||
else:
|
||||
pip_exe = str(venv_dir / "bin" / "pip")
|
||||
python_exe = str(venv_dir / "bin" / "python")
|
||||
dist_extras = ", ".join(stubtest_settings.extras)
|
||||
dist_req = f"{dist_name}[{dist_extras}]=={metadata.version}"
|
||||
|
||||
|
||||
@@ -4,13 +4,10 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import venv
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any, Final, NamedTuple
|
||||
from typing_extensions import Annotated
|
||||
|
||||
import pathspec
|
||||
|
||||
@@ -51,34 +48,11 @@ def print_success_msg() -> None:
|
||||
# ====================================================================
|
||||
|
||||
|
||||
class VenvInfo(NamedTuple):
|
||||
pip_exe: Annotated[str, "A path to the venv's pip executable"]
|
||||
python_exe: Annotated[str, "A path to the venv's python executable"]
|
||||
|
||||
@staticmethod
|
||||
def of_existing_venv(venv_dir: Path) -> VenvInfo:
|
||||
if sys.platform == "win32":
|
||||
pip = venv_dir / "Scripts" / "pip.exe"
|
||||
python = venv_dir / "Scripts" / "python.exe"
|
||||
else:
|
||||
pip = venv_dir / "bin" / "pip"
|
||||
python = venv_dir / "bin" / "python"
|
||||
|
||||
return VenvInfo(str(pip), str(python))
|
||||
|
||||
|
||||
def make_venv(venv_dir: Path) -> VenvInfo:
|
||||
try:
|
||||
venv.create(venv_dir, with_pip=True, clear=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
if "ensurepip" in e.cmd and b"KeyboardInterrupt" not in e.stdout.splitlines():
|
||||
print_error(
|
||||
"stubtest requires a Python installation with ensurepip. "
|
||||
"If on Linux, you may need to install the python3-venv package."
|
||||
)
|
||||
raise
|
||||
|
||||
return VenvInfo.of_existing_venv(venv_dir)
|
||||
@cache
|
||||
def venv_python(venv_dir: Path) -> Path:
|
||||
if sys.platform == "win32":
|
||||
return venv_dir / "Scripts" / "python.exe"
|
||||
return venv_dir / "bin" / "python"
|
||||
|
||||
|
||||
@cache
|
||||
|
||||
Reference in New Issue
Block a user