python-project-generator 3.2.2

Generates a Python project structure.
use anyhow::{bail, Result};

use crate::{
    package_version::PythonPackage,
    project_generator::{determine_dev_packages, format_package_with_extras},
    project_info::{ProjectInfo, ProjectManager, Pyo3PythonManager},
};

pub fn install_dev_dependencies(project_info: &ProjectInfo) -> Result<()> {
    match project_info.project_manager {
        ProjectManager::Uv => uv_dev_dependency_installer(project_info)?,
        ProjectManager::Poetry => poetry_dev_dependency_installer(project_info)?,
        ProjectManager::Setuptools => setuptools_dev_dependency_installer(project_info)?,
        ProjectManager::Maturin => maturin_dev_dependency_installer(project_info)?,
    };

    Ok(())
}

fn uv_dev_dependency_installer(project_info: &ProjectInfo) -> Result<()> {
    let packages = determine_dev_packages(project_info)?;
    let package_specs: Vec<String> = packages.iter().map(format_package_with_extras).collect();

    let mut args = vec!["add", "--group=dev"];
    let package_refs: Vec<&str> = package_specs.iter().map(|s| s.as_str()).collect();
    args.extend(package_refs);

    let output = std::process::Command::new("uv")
        .args(args)
        .current_dir(project_info.base_dir())
        .output()?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        bail!("Failed to install dev dependencies: {stderr}");
    }

    Ok(())
}

fn poetry_dev_dependency_installer(project_info: &ProjectInfo) -> Result<()> {
    let packages = determine_dev_packages(project_info)?;

    for package in packages {
        let package_spec = format_package_with_extras(&package);
        let mut args = vec!["add", "--group=dev"];

        if package == PythonPackage::Tomli {
            args.extend(&[&package_spec, "--python", "<3.11"]);
        } else {
            args.push(&package_spec);
        }

        let output = std::process::Command::new("poetry")
            .args(args)
            .current_dir(project_info.base_dir())
            .output()?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            bail!("Failed to install {}: {stderr}", package);
        }
    }

    Ok(())
}

fn setuptools_dev_dependency_installer(project_info: &ProjectInfo) -> Result<()> {
    let base_dir = project_info.base_dir().canonicalize()?;
    let venv_path = base_dir.join(".venv");
    if !venv_path.exists() {
        let venv_output = std::process::Command::new("python")
            .args(["-m", "venv", ".venv"])
            .current_dir(project_info.base_dir())
            .output()?;

        if !venv_output.status.success() {
            let stderr = String::from_utf8_lossy(&venv_output.stderr);
            bail!("Failed to create virtual environment: {stderr}");
        }
    }

    let python_bin = if cfg!(windows) {
        venv_path.join("Scripts").join("python.exe")
    } else {
        venv_path.join("bin").join("python")
    };

    let packages = determine_dev_packages(project_info)?;
    let package_specs: Vec<String> = packages.iter().map(format_package_with_extras).collect();

    let mut args = vec!["-m", "pip", "install"];
    let package_refs: Vec<&str> = package_specs.iter().map(|s| s.as_str()).collect();
    args.extend(package_refs);

    let output = std::process::Command::new(&python_bin)
        .args(args)
        .current_dir(project_info.base_dir())
        .output()?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        bail!("Failed to install dev dependencies: {stderr}");
    }

    let freeze = std::process::Command::new(python_bin)
        .args(["-m", "pip", "freeze"])
        .current_dir(project_info.base_dir())
        .output()?;

    if !freeze.status.success() {
        let stderr = String::from_utf8_lossy(&freeze.stderr);
        bail!("Failed to get pip freeze output: {stderr}");
    }

    let requirements_path = project_info.base_dir().join("requirements-dev.txt");
    std::fs::write(requirements_path, freeze.stdout)?;

    Ok(())
}

fn maturin_dev_dependency_installer(project_info: &ProjectInfo) -> Result<()> {
    if let Some(pyo3_python_manager) = &project_info.pyo3_python_manager {
        match pyo3_python_manager {
            Pyo3PythonManager::Uv => uv_dev_dependency_installer(project_info),
            Pyo3PythonManager::Setuptools => setuptools_dev_dependency_installer(project_info),
        }
    } else {
        bail!("No Python project manager provided for PyO3 project");
    }
}

pub fn update_prek_hooks(project_info: &ProjectInfo) -> Result<()> {
    match project_info.project_manager {
        ProjectManager::Uv => uv_prek_autoupdate(project_info)?,
        ProjectManager::Poetry => poetry_prek_autoupdate(project_info)?,
        ProjectManager::Setuptools => setuptools_prek_autoupdate(project_info)?,
        ProjectManager::Maturin => maturin_prek_autoupdate(project_info)?,
    };

    Ok(())
}

fn uv_prek_autoupdate(project_info: &ProjectInfo) -> Result<()> {
    let output = std::process::Command::new("uv")
        .args(["run", "prek", "autoupdate"])
        .current_dir(project_info.base_dir())
        .output()?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        bail!("Failed to update prek hooks: {stderr}");
    }

    Ok(())
}

fn poetry_prek_autoupdate(project_info: &ProjectInfo) -> Result<()> {
    let output = std::process::Command::new("poetry")
        .args(["run", "prek", "autoupdate"])
        .current_dir(project_info.base_dir())
        .output()?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        bail!("Failed to update prek hooks: {stderr}");
    }

    Ok(())
}

fn setuptools_prek_autoupdate(project_info: &ProjectInfo) -> Result<()> {
    let base_dir = project_info.base_dir().canonicalize()?;
    let venv_path = base_dir.join(".venv");

    if !venv_path.exists() {
        bail!("Virtual environment not found at {}", venv_path.display());
    }

    let python_bin = if cfg!(windows) {
        venv_path.join("Scripts").join("prek.exe")
    } else {
        venv_path.join("bin").join("prek")
    };

    let output = std::process::Command::new(&python_bin)
        .args(["autoupdate"])
        .current_dir(base_dir)
        .output()?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        bail!("Failed to update prek hooks: {stderr}");
    }

    Ok(())
}

fn maturin_prek_autoupdate(project_info: &ProjectInfo) -> Result<()> {
    if let Some(pyo3_python_manager) = &project_info.pyo3_python_manager {
        match pyo3_python_manager {
            Pyo3PythonManager::Uv => uv_prek_autoupdate(project_info),
            Pyo3PythonManager::Setuptools => setuptools_prek_autoupdate(project_info),
        }
    } else {
        bail!("No Python project manager provided for PyO3 project");
    }
}

pub fn install_prek_hooks(project_info: &ProjectInfo) -> Result<()> {
    match project_info.project_manager {
        ProjectManager::Uv => uv_prek_install(project_info)?,
        ProjectManager::Poetry => poetry_prek_install(project_info)?,
        ProjectManager::Setuptools => setuptools_prek_install(project_info)?,
        ProjectManager::Maturin => maturin_prek_install(project_info)?,
    };

    Ok(())
}

fn uv_prek_install(project_info: &ProjectInfo) -> Result<()> {
    let output = std::process::Command::new("uv")
        .args(["run", "prek", "install"])
        .current_dir(project_info.base_dir())
        .output()?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        bail!("Failed to install prek hooks: {stderr}");
    }

    Ok(())
}

fn poetry_prek_install(project_info: &ProjectInfo) -> Result<()> {
    let output = std::process::Command::new("poetry")
        .args(["run", "prek", "install"])
        .current_dir(project_info.base_dir())
        .output()?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        bail!("Failed to install prek hooks: {stderr}");
    }

    Ok(())
}

fn setuptools_prek_install(project_info: &ProjectInfo) -> Result<()> {
    let base_dir = project_info.base_dir().canonicalize()?;
    let venv_path = base_dir.join(".venv");

    if !venv_path.exists() {
        bail!("Virtual environment not found at {}", venv_path.display());
    }

    let python_bin = if cfg!(windows) {
        venv_path.join("Scripts").join("prek.exe")
    } else {
        venv_path.join("bin").join("prek")
    };

    let output = std::process::Command::new(&python_bin)
        .args(["install"])
        .current_dir(base_dir)
        .output()?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        bail!("Failed to install prek hooks: {stderr}");
    }

    Ok(())
}

fn maturin_prek_install(project_info: &ProjectInfo) -> Result<()> {
    if let Some(pyo3_python_manager) = &project_info.pyo3_python_manager {
        match pyo3_python_manager {
            Pyo3PythonManager::Uv => uv_prek_install(project_info),
            Pyo3PythonManager::Setuptools => setuptools_prek_install(project_info),
        }
    } else {
        bail!("No Python project manager provided for PyO3 project");
    }
}