Coverage for coffee_maker/utils/setup_isolated_venv.py: 93%
69 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-21 05:58 +0000
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-21 05:58 +0000
1import logging
2import pathlib
3import shutil
4import subprocess
5import sys
7logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
8logger = logging.getLogger(__name__)
11def get_venv_python_executable(venv_dir_path: pathlib.Path) -> str:
12 """Gets the path to the Python executable in the virtual environment.
13 Args:
14 venv_dir_path (pathlib.Path): Path to the virtual environment directory.
15 Returns:
16 str: Path to the Python executable in the virtual environment.
17 """
18 if sys.platform == "win32":
19 return str(venv_dir_path / "Scripts" / "python.exe")
20 else:
21 return str(venv_dir_path / "bin" / "python")
24def setup_uv_pip(
25 venv_dir_path: pathlib.Path = None,
26 packages_to_install: list[str] = None,
27 python_version: str = None,
28 overwrite_existing: bool = True,
29) -> str: # Return type is str, not None
30 """
31 Creates a virtual environment and installs a list of packages if not already set up.
32 Uses 'uv' for environment and package management.
33 Args:
34 venv_dir_path (pathlib.Path): Path to the virtual environment directory.
35 packages_to_install (list[str]): List of packages to install in the virtual environment.
36 python_version (str): Python version to use for the virtual environment.
37 overwrite_existing (bool): Whether to overwrite an existing virtual environment.
38 Returns:
39 str: Path to the Python executable in the virtual environment.
40 Raises:
41 RuntimeError: If setting up the environment or installing packages fails.
42 FileNotFoundError: If 'uv' command is not found.
43 """
45 if venv_dir_path is None:
46 raise ValueError("venv_dir_path must be provided")
47 if packages_to_install is None:
48 raise ValueError("packages_to_install must be provided")
49 if python_version is None:
50 raise ValueError("python_version must be provided")
52 venv_python_executable = get_venv_python_executable(venv_dir_path)
53 needs_venv_creation = False
55 if venv_dir_path.exists():
56 if overwrite_existing:
57 logger.info(f"Virtual environment '{venv_dir_path}' exists. Overwriting as requested.")
58 try:
59 shutil.rmtree(str(venv_dir_path))
60 except OSError as e:
61 logger.error(f"Error removing existing venv '{venv_dir_path}': {e}")
62 raise RuntimeError(f"Failed to remove existing venv '{venv_dir_path}'.") from e
63 needs_venv_creation = True
64 elif not pathlib.Path(venv_python_executable).exists():
65 logger.warning(
66 f"Virtual environment '{venv_dir_path}' exists but is incomplete (Python executable missing). "
67 f"Recreating."
68 )
69 try:
70 shutil.rmtree(str(venv_dir_path)) # Clean up potentially corrupted venv
71 except OSError as e:
72 logger.error(f"Error removing incomplete venv '{venv_dir_path}': {e}")
73 raise RuntimeError(f"Failed to remove incomplete venv '{venv_dir_path}'.") from e
74 needs_venv_creation = True
75 else:
76 logger.info(
77 f"Virtual environment '{venv_dir_path}' already exists and Python executable found. "
78 f"Skipping venv creation step."
79 )
80 # venv exists and is complete, and not overwriting
81 else:
82 logger.info(f"Virtual environment '{venv_dir_path}' not found. Will create.")
83 needs_venv_creation = True
85 try:
86 if needs_venv_creation:
87 logger.info(f"Creating virtual environment at '{venv_dir_path}' using Python {python_version}.")
88 # Create venv
89 subprocess.run(
90 ["uv", "venv", str(venv_dir_path), f"--python={python_version}"],
91 check=True,
92 capture_output=True,
93 text=True,
94 )
95 logger.info(f"Virtual environment '{venv_dir_path}' created successfully with Python {python_version}.")
96 else:
97 logger.info(f"Using existing virtual environment at '{venv_dir_path}'.")
99 # Install packages in the venv
100 # 'uv pip install' is generally idempotent, so it's okay to run this even if packages might exist.
101 # It will ensure they are present.
102 for package in packages_to_install:
103 # Ensure venv_python_executable is valid before using it for install
104 if not pathlib.Path(venv_python_executable).exists():
105 # This case should ideally be caught by the venv creation logic if needs_venv_creation was true
106 # or by the initial check if needs_venv_creation was false.
107 # Adding a safeguard here.
108 logger.error(
109 f"Python executable '{venv_python_executable}' not found before package installation. "
110 f"Venv setup might have failed unexpectedly."
111 )
112 raise RuntimeError(f"Venv Python executable not found at '{venv_python_executable}'.")
114 install_command = ["uv", "pip", "install", "--python", venv_python_executable, package]
115 logger.info(f"Installing/verifying {package} using command: {' '.join(install_command)}")
116 result = subprocess.run(install_command, check=True, capture_output=True, text=True)
117 # uv might not produce much stdout for already satisfied requirements, which is fine.
118 if result.stdout.strip():
119 logger.info(f"Output for {package} installation:\n{result.stdout.strip()}")
120 else:
121 logger.info(f"{package} is up to date or installed successfully in '{venv_dir_path}'.")
123 except subprocess.CalledProcessError as e:
124 logger.error(f"Error during virtual environment operation for '{venv_dir_path}':")
125 logger.error(f"Command: {' '.join(e.cmd)}")
126 logger.error(f"Return code: {e.returncode}")
127 if e.stdout:
128 logger.error(f"Stdout: {e.stdout.strip()}")
129 if e.stderr:
130 logger.error(f"Stderr: {e.stderr.strip()}")
131 raise RuntimeError(f"Failed to set up or update virtual environment '{venv_dir_path}'.") from e
132 except FileNotFoundError:
133 logger.error("`uv` command not found. Please ensure `uv` is installed and in your PATH.")
134 # Re-raise the FileNotFoundError to be handled by the caller if needed,
135 # or to clearly indicate 'uv' is the issue.
136 raise
138 return venv_python_executable