#!/usr/bin/env python3 # === Standard library === import os import sys import subprocess import venv from pathlib import Path from typing import Callable from argparse import ArgumentParser, Namespace # === Configuration === # Define the virtual environment directory in the user's home folder VENV_DIR: Path = Path.home() / ".scripts_fabq_venv" # Define the path to the requirements.txt file in the project root REQUIREMENTS_FILE: Path = Path(__file__).resolve().parents[2] / "requirements.txt" def parse_verbose() -> Namespace: """ Parse the --verbose flag before virtual environment setup. This is useful when argument parsing requires packages that may only be available after the virtual environment is initialized. Returns: Namespace: Contains a single attribute 'verbose' (bool). """ parser = ArgumentParser(add_help=False) parser.add_argument( "-v", "--verbose", action="count", default=0, help="Increase verbosity: -v = verbose, -vv = debug, -vvv = trace", ) parser.add_argument( "--debug", dest="verbose", action="store_const", const=2, help="Enable debug output (equivalent to -vv)", ) parser.add_argument( "--trace", dest="verbose", action="store_const", const=3, help="Enable full trace output (equivalent to -vvv)", ) return parser.parse_known_args()[0] def is_in_venv() -> bool: """ Determine whether the script is currently running inside a virtual environment. Returns: bool: True if running inside a virtual environment, False otherwise. """ return hasattr(sys, "real_prefix") or sys.prefix != getattr( sys, "base_prefix", sys.prefix ) def create_venv(verbose: bool = False) -> None: """ Create a virtual environment in the predefined VENV_DIR location. Args: verbose (bool): If True, prints progress messages. """ if verbose >= 2: print(f"Creating virtual environment in {VENV_DIR}...") venv.create(VENV_DIR, with_pip=True) def install_requirements(verbose: bool = False) -> None: """ Install Python dependencies from requirements.txt into the venv. Args: verbose (bool): If True, prints progress and pip output. """ if not REQUIREMENTS_FILE.exists(): print(f"Error: requirements.txt not found at {REQUIREMENTS_FILE}") sys.exit(1) if verbose >= 2: print(f"Installing/updating requirements from {REQUIREMENTS_FILE}...") pip_cmd = [str(VENV_DIR / "bin" / "pip")] try: subprocess.check_call( pip_cmd + ["install", "-r", str(REQUIREMENTS_FILE)], stdout=None if verbose >= 2 else subprocess.DEVNULL, stderr=None if verbose >= 2 else subprocess.DEVNULL, ) except subprocess.CalledProcessError as e: print(f"Error: Failed to install requirements: {e}") sys.exit(1) try: subprocess.check_call( pip_cmd + ["check"], stdout=None if verbose >= 2 else subprocess.DEVNULL, stderr=None if verbose >= 2 else subprocess.DEVNULL, ) except subprocess.CalledProcessError: print("Error: Dependency conflict detected — your environment may be broken.") raise def run_in_venv(main_func: Callable[[], None], verbose: bool = False) -> None: """ Ensure the script is running inside the expected virtual environment. If not already in the venv: - Create it if needed - Install requirements - Relaunch the script inside the venv If already in the venv: - Ensure requirements are installed - Execute the given main function Args: main_func (Callable[[], None]): The function to execute after setup. verbose (bool): If True, enables progress messages and pip output. """ if not is_in_venv(): if not VENV_DIR.exists(): create_venv(verbose) install_requirements(verbose) # Relaunch the script inside the virtual environment os.execv( str(VENV_DIR / "bin" / "python"), [str(VENV_DIR / "bin" / "python")] + sys.argv, ) else: install_requirements(verbose) main_func()