Skip to content

Commit

Permalink
Use uv to install packages dynamically in mypy_test.py, `regr_tes…
Browse files Browse the repository at this point in the history
…t.py` and `stubtest_third_party.py`
  • Loading branch information
AlexWaygood committed Mar 1, 2024
1 parent 9e5bced commit 9541c25
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 91 deletions.
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
40 changes: 19 additions & 21 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, venv_python


def run_stubtest(
Expand Down Expand Up @@ -42,20 +42,18 @@ def run_stubtest(
return True

with tempfile.TemporaryDirectory() as tmp:
venv_dir = Path(tmp)
try:
pip_exe, python_exe = make_venv(venv_dir)
except Exception:
print_error("fail")
raise
venv_dir = Path(tmp) / ".venv"
subprocess.run(["uv", "venv", venv_dir, "--quiet"], check=True, capture_output=True)
python_exe = venv_python(venv_dir)
env_vars = os.environ | {"VIRTUAL_ENV": str(venv_dir)}
dist_extras = ", ".join(stubtest_settings.extras)
dist_req = f"{dist_name}[{dist_extras}]=={metadata.version}"

# If tool.stubtest.stubtest_requirements exists, run "pip install" on it.
# If tool.stubtest.stubtest_requirements exists, run "uv install" on it.
if stubtest_settings.stubtest_requirements:
pip_cmd = [pip_exe, "install", *stubtest_settings.stubtest_requirements]
uv_cmd = ["uv", "pip", "install", *stubtest_settings.stubtest_requirements]
try:
subprocess.run(pip_cmd, check=True, capture_output=True)
subprocess.run(uv_cmd, check=True, capture_output=True, env=env_vars)
except subprocess.CalledProcessError as e:
print_command_failure("Failed to install requirements", e)
return False
Expand All @@ -67,9 +65,9 @@ def run_stubtest(
# TODO: Maybe find a way to cache these in CI
dists_to_install = [dist_req, get_mypy_req()]
dists_to_install.extend(requirements.external_pkgs) # Internal requirements are added to MYPYPATH
pip_cmd = [pip_exe, "install", *dists_to_install]
uv_cmd = ["uv", "pip", "install", *dists_to_install]
try:
subprocess.run(pip_cmd, check=True, capture_output=True)
subprocess.run(uv_cmd, check=True, capture_output=True, env=env_vars)
except subprocess.CalledProcessError as e:
print_command_failure("Failed to install", e)
return False
Expand All @@ -78,7 +76,7 @@ def run_stubtest(
packages_to_check = [d.name for d in dist.iterdir() if d.is_dir() and d.name.isidentifier()]
modules_to_check = [d.stem for d in dist.iterdir() if d.is_file() and d.suffix == ".pyi"]
stubtest_cmd = [
python_exe,
str(python_exe),
"-m",
"mypy.stubtest",
# Use --custom-typeshed-dir in case we make linked changes to stdlib or _typeshed
Expand All @@ -98,7 +96,7 @@ def run_stubtest(
# It seems that some other environment variables are needed too,
# because the CI fails if we pass only os.environ["DISPLAY"]. I didn't
# "bisect" to see which variables are actually needed.
stubtest_env = os.environ | {"MYPYPATH": mypypath, "MYPY_FORCE_COLOR": "1"}
env_vars |= {"MYPYPATH": mypypath, "MYPY_FORCE_COLOR": "1"}

allowlist_path = dist / "@tests/stubtest_allowlist.txt"
if allowlist_path.exists():
Expand All @@ -113,18 +111,18 @@ def run_stubtest(
return False

try:
subprocess.run(stubtest_cmd, env=stubtest_env, check=True, capture_output=True)
subprocess.run(stubtest_cmd, env=env_vars, check=True, capture_output=True)
except subprocess.CalledProcessError as e:
print_error("fail")
print_commands(dist, pip_cmd, stubtest_cmd, mypypath)
print_commands(dist, uv_cmd, stubtest_cmd, mypypath)
print_command_output(e)

print("Python version: ", file=sys.stderr)
ret = subprocess.run([sys.executable, "-VV"], capture_output=True)
print_command_output(ret)

print("Ran with the following environment:", file=sys.stderr)
ret = subprocess.run([pip_exe, "freeze", "--all"], capture_output=True)
ret = subprocess.run(["uv", "pip", "freeze"], capture_output=True, env=env_vars)
print_command_output(ret)

if allowlist_path.exists():
Expand All @@ -134,15 +132,15 @@ def run_stubtest(
print(file=sys.stderr)
else:
print(f"Re-running stubtest with --generate-allowlist.\nAdd the following to {allowlist_path}:", file=sys.stderr)
ret = subprocess.run([*stubtest_cmd, "--generate-allowlist"], env=stubtest_env, capture_output=True)
ret = subprocess.run([*stubtest_cmd, "--generate-allowlist"], env=env_vars, capture_output=True)
print_command_output(ret)

return False
else:
print_success_msg()

if verbose:
print_commands(dist, pip_cmd, stubtest_cmd, mypypath)
print_commands(dist, uv_cmd, stubtest_cmd, mypypath)

return True

Expand Down Expand Up @@ -223,9 +221,9 @@ def setup_uwsgi_stubtest_command(dist: Path, venv_dir: Path, stubtest_cmd: list[
return True


def print_commands(dist: Path, pip_cmd: list[str], stubtest_cmd: list[str], mypypath: str) -> None:
def print_commands(dist: Path, uv_cmd: list[str], stubtest_cmd: list[str], mypypath: str) -> None:
print(file=sys.stderr)
print(" ".join(pip_cmd), file=sys.stderr)
print(" ".join(uv_cmd), file=sys.stderr)
print(f"MYPYPATH={mypypath}", " ".join(stubtest_cmd), file=sys.stderr)
print(file=sys.stderr)

Expand Down
Loading

0 comments on commit 9541c25

Please sign in to comment.