Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use uv to install packages into venvs in regr_test.py #26

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions requirements-tests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
68 changes: 36 additions & 32 deletions tests/mypy_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,14 @@
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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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()
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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,
Expand All @@ -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))


Expand Down Expand Up @@ -409,22 +408,30 @@ def merge(self, other: TestSummary) -> None:


_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
Expand All @@ -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

Expand All @@ -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:
Expand All @@ -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)
Expand All @@ -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

Expand All @@ -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)

Expand Down Expand Up @@ -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)

Expand Down
16 changes: 9 additions & 7 deletions tests/regr_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,12 @@
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
14 changes: 8 additions & 6 deletions tests/stubtest_third_party.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
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(
Expand Down Expand Up @@ -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}"

Expand Down
36 changes: 5 additions & 31 deletions tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@

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

Expand Down Expand Up @@ -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
Expand Down
Loading