diff --git a/tests/requirements.txt b/tests/requirements.txt index 9d0879b..6252065 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,5 +1,5 @@ -e .. -virtualenv +uv maturin==1.5.0 pytest junit2html diff --git a/tests/runner.py b/tests/runner.py index c5e7bcf..db58b66 100644 --- a/tests/runner.py +++ b/tests/runner.py @@ -100,16 +100,33 @@ def _run_test_in_environment( sys.exit(proc.returncode) +def _pip_install_command(interpreter_path: Path) -> list[str]: + if shutil.which("uv") is not None: + log.info("using uv to install packages") + return [ + "uv", + "pip", + "install", + "--python", + str(interpreter_path), + ] + else: + log.info("using pip to install packages") + return [ + str(interpreter_path), + "-m", + "pip", + "install", + "--disable-pip-version-check", + ] + + def _create_test_venv(python: Path, venv_dir: Path) -> VirtualEnv: venv = VirtualEnv.new(venv_dir, python) log.info("installing test requirements into virtualenv") proc = subprocess.run( [ - str(venv.interpreter_path), - "-m", - "pip", - "install", - "--disable-pip-version-check", + *_pip_install_command(venv.interpreter_path), "-r", "requirements.txt", ], @@ -120,13 +137,25 @@ def _create_test_venv(python: Path, venv_dir: Path) -> VirtualEnv: if proc.returncode != 0: log.error(proc.stdout.decode()) log.error(proc.stderr.decode()) - msg = "pip install failed" + msg = "package installation failed" raise RuntimeError(msg) log.debug("%s", proc.stdout.decode()) log.info("test environment ready") return venv +def _create_virtual_env_command(interpreter_path: Path, venv_path: Path) -> list[str]: + if shutil.which("uv") is not None: + log.info("using uv to create virtual environments") + return ["uv", "venv", "--seed", "--python", str(interpreter_path), str(venv_path)] + elif shutil.which("virtualenv") is not None: + log.info("using virtualenv to create virtual environments") + return ["virtualenv", "--python", str(interpreter_path), str(venv_path)] + else: + log.info("using venv to create virtual environments") + return [str(interpreter_path), "-m", "venv", str(venv_path)] + + class VirtualEnv: def __init__(self, root: Path) -> None: self._root = root.resolve() @@ -140,7 +169,7 @@ def new(root: Path, interpreter_path: Path) -> VirtualEnv: if not interpreter_path.exists(): raise FileNotFoundError(interpreter_path) log.info("creating test virtualenv at '%s' from '%s'", root, interpreter_path) - cmd = ["virtualenv", "--python", str(interpreter_path), str(root)] + cmd = _create_virtual_env_command(interpreter_path, root) proc = subprocess.run(cmd, capture_output=True, check=True) log.debug("%s", proc.stdout.decode()) assert root.is_dir() diff --git a/tests/test_import_hook/test_project_importer.py b/tests/test_import_hook/test_project_importer.py index f40556d..b00425a 100644 --- a/tests/test_import_hook/test_project_importer.py +++ b/tests/test_import_hook/test_project_importer.py @@ -1342,30 +1342,47 @@ def _rebuilt_message(project_name: str) -> str: return f'rebuilt and loaded package "{with_underscores(project_name)}"' +_UV_AVAILABLE = None + + +def uv_available() -> bool: + """whether the `uv` command is installed""" + global _UV_AVAILABLE + if _UV_AVAILABLE is None: + _UV_AVAILABLE = shutil.which("uv") is not None + return _UV_AVAILABLE + + def _uninstall(*project_names: str) -> None: log.info("uninstalling %s", sorted(project_names)) - subprocess.check_call([ - sys.executable, - "-m", - "pip", - "uninstall", - "--disable-pip-version-check", - "-y", - *project_names, - ]) + if uv_available(): + cmd = ["uv", "pip", "uninstall", "--python", str(sys.executable), *project_names] + else: + cmd = [ + sys.executable, + "-m", + "pip", + "uninstall", + "--disable-pip-version-check", + "-y", + *project_names, + ] + subprocess.check_call(cmd) def _get_installed_package_names() -> set[str]: - packages = json.loads( - subprocess.check_output([ + if uv_available(): + cmd = ["uv", "pip", "list", "--python", sys.executable, "--format", "json"] + else: + cmd = [ sys.executable, "-m", "pip", "--disable-pip-version-check", "list", "--format=json", - ]).decode() - ) + ] + packages = json.loads(subprocess.check_output(cmd).decode()) return {package["name"] for package in packages} @@ -1382,7 +1399,11 @@ def _install_editable(project_dir: Path) -> None: def _install_non_editable(project_dir: Path) -> None: log.info("installing %s in non-editable mode", project_dir.name) - subprocess.check_call([sys.executable, "-m", "pip", "install", "--disable-pip-version-check", str(project_dir)]) + if uv_available(): + cmd = ["uv", "pip", "install", "--python", sys.executable, str(project_dir)] + else: + cmd = [sys.executable, "-m", "pip", "install", "--disable-pip-version-check", str(project_dir)] + subprocess.check_call(cmd) def _is_installed_as_pth(project_name: str) -> bool: @@ -1415,14 +1436,20 @@ def _is_editable_installed_correctly(project_name: str, project_dir: Path, is_mi installed_editable_with_direct_url, ) + if uv_available(): + # TODO(matt): use uv once the --files option is supported https://github.com/astral-sh/uv/issues/2526 + cmd = [sys.executable, "-m", "pip", "show", "--disable-pip-version-check", "-f", project_name] + else: + cmd = [sys.executable, "-m", "pip", "show", "--disable-pip-version-check", "-f", project_name] + proc = subprocess.run( - [sys.executable, "-m", "pip", "show", "--disable-pip-version-check", "-f", project_name], + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False, ) output = "None" if proc.stdout is None else proc.stdout.decode() - log.info("pip output (returned %s):\n%s", proc.returncode, output) + log.info("command output (returned %s):\n%s", proc.returncode, output) return installed_editable_with_direct_url and (installed_as_pth == is_mixed)