diff --git a/src/lib.rs b/src/lib.rs index ccfa6b0..1be80d2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ use clap::{Parser, Subcommand}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; +use std::process::Command; use std::path::Path; #[derive(Parser)] @@ -15,7 +16,7 @@ pub struct Cli { #[derive(Subcommand)] pub enum Commands { - Install { name: Option, version: Option, requirements: Option }, + Install { packages: Vec }, Delete { name: String }, Update { name: String, version: String }, List, @@ -62,8 +63,6 @@ pub fn run_command(_command: &str) -> bool { #[cfg(not(test))] pub fn run_command(command: &str) -> bool { - use std::process::Command; - if cfg!(target_os = "windows") { Command::new("cmd") .args(&["/C", command]) @@ -80,18 +79,15 @@ pub fn run_command(command: &str) -> bool { } } -pub fn install_package(name: &str, version: Option<&str>, packages: &mut PackageRegistry) { - let version = version.unwrap_or("latest"); - let install_command = if version == "latest" { - format!("pip install {}", name) - } else { - format!("pip install {}=={}", name, version) - }; - if run_command(&install_command) { - packages.packages.insert(name.to_string(), version.to_string()); - println!("Package {} installed successfully", name); - } else { - println!("Failed to install package {}", name); +pub fn install_packages(package_names: &[String], packages: &mut PackageRegistry) { + for name in package_names { + let install_command = format!("pip install {}", name); + if run_command(&install_command) { + packages.packages.insert(name.clone(), "latest".to_string()); + println!("Package {} installed successfully", name); + } else { + println!("Failed to install package {}", name); + } } } @@ -118,7 +114,13 @@ pub fn install_from_requirements(requirements: &str, packages: &mut PackageRegis if let Ok(data) = fs::read_to_string(requirements) { if let Ok(registry) = serde_json::from_str::(&data) { for (name, version) in registry.packages { - install_package(&name, Some(&version), packages); + let install_command = format!("pip install {}=={}", name, version); + if run_command(&install_command) { + packages.packages.insert(name.clone(), version.clone()); + println!("Package {} installed successfully", name); + } else { + println!("Failed to install package {}", name); + } } } else { println!("Failed to parse the requirements file."); diff --git a/src/main.rs b/src/main.rs index e86385c..fd06482 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,30 +1,29 @@ use clap::Parser; -use python_package_manager::{Cli, Commands, load_packages, save_packages, install_package, delete_package, update_package, list_packages, install_from_requirements}; +use python_package_manager::{Cli, Commands, load_packages, save_packages, install_packages, delete_package, update_package, list_packages, install_from_requirements}; fn main() { let args = Cli::parse(); - let mut packages = load_packages(); + let mut package_registry = load_packages(); match args.command { - Commands::Install { name, version, requirements } => { - if let Some(requirements) = requirements { - install_from_requirements(&requirements, &mut packages); - } else if let Some(name) = name { - install_package(&name, version.as_deref(), &mut packages); + Commands::Install { packages } => { + if packages.len() == 1 && packages[0].starts_with("-r=") { + let requirements_path = &packages[0][3..]; + install_from_requirements(requirements_path, &mut package_registry); } else { - println!("You must specify a package name or a requirements file."); + install_packages(&packages, &mut package_registry); } } Commands::Delete { name } => { - delete_package(&name, &mut packages); + delete_package(&name, &mut package_registry); } Commands::Update { name, version } => { - update_package(&name, &version, &mut packages); + update_package(&name, &version, &mut package_registry); } Commands::List => { - list_packages(&packages); + list_packages(&package_registry); } } - save_packages(&packages); + save_packages(&package_registry); } diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index d9efa0b..8b464af 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1,6 +1,87 @@ use std::fs; +use std::path::Path; +use std::process::{Command, Output}; use tempfile::tempdir; -use python_package_manager::{PackageRegistry, load_packages_from_path, save_packages_to_path, install_package, delete_package, update_package, list_packages}; +use python_package_manager::{PackageRegistry, load_packages_from_path, save_packages_to_path, install_packages, delete_package, update_package, list_packages, install_from_requirements}; + +fn get_python_interpreter() -> String { + let version_output: Output = if cfg!(target_os = "windows") { + Command::new("cmd") + .args(&["/C", "where python"]) + .output() + .expect("Failed to execute command") + } else { + Command::new("sh") + .arg("-c") + .arg("which python3") + .output() + .expect("Failed to execute command") + }; + + if !version_output.status.success() { + panic!("Failed to find Python interpreter: {}", String::from_utf8_lossy(&version_output.stderr)); + } + + let interpreter_path = String::from_utf8_lossy(&version_output.stdout) + .lines() + .next() + .expect("No Python interpreter found") + .to_string(); + + // Verify the Python version is 3.10 or above + let version_check = Command::new(&interpreter_path) + .arg("--version") + .output() + .expect("Failed to check Python version"); + + if !version_check.status.success() { + panic!("Failed to check Python version: {}", String::from_utf8_lossy(&version_check.stderr)); + } + + let version_str = String::from_utf8_lossy(&version_check.stdout); + if version_str.contains("Python 3.10") || version_str.contains("Python 3.11") { + interpreter_path + } else { + panic!("Python version 3.10 or above is required. Found: {}", version_str); + } +} + +fn create_virtualenv(env_path: &Path) { + let python_interpreter = get_python_interpreter(); + + let output = Command::new(&python_interpreter) + .args(&["-m", "venv", env_path.to_str().unwrap()]) + .output() + .expect("Failed to create virtual environment"); + + if !output.status.success() { + panic!( + "Failed to create virtual environment: {}", + String::from_utf8_lossy(&output.stderr) + ); + } +} + +fn run_command_in_virtualenv(env_path: &Path, args: &[&str]) -> bool { + let bin_path = if cfg!(target_os = "windows") { + env_path.join("Scripts").join("pip") + } else { + env_path.join("bin").join("pip") + }; + + let output = Command::new(bin_path) + .args(args) + .output() + .expect("Failed to run command in virtual environment"); + + if !output.status.success() { + println!("Command failed: {}", String::from_utf8_lossy(&output.stderr)); + } else { + println!("Command succeeded: {}", String::from_utf8_lossy(&output.stdout)); + } + + output.status.success() +} #[test] fn test_load_empty_packages() { @@ -26,7 +107,7 @@ fn test_load_packages() { fn test_save_packages() { let dir = tempdir().expect("Failed to create temp dir"); let file_path = dir.path().join("requirements.json"); - let mut packages = PackageRegistry::new(); + let mut packages: PackageRegistry = PackageRegistry::new(); packages.packages.insert("pandas".to_string(), "1.0.0".to_string()); save_packages_to_path(&packages, &file_path); let data = fs::read_to_string(&file_path).expect("Failed to read from temp file"); @@ -38,41 +119,71 @@ fn test_save_packages() { } #[test] -fn test_install_package() { +fn test_install_packages() { let dir = tempdir().expect("Failed to create temp dir"); - let file_path: std::path::PathBuf = dir.path().join("requirements.json"); + let env_path = dir.path().join("env"); + create_virtualenv(&env_path); + + let file_path = dir.path().join("requirements.json"); fs::write(&file_path, "{}").expect("Failed to write to temp file"); + let mut packages = PackageRegistry::new(); - install_package("pandas", Some("latest"), &mut packages); + let package_list = vec!["pandas".to_string(), "numpy".to_string(), "scipy".to_string()]; + for package in &package_list { + if !run_command_in_virtualenv(&env_path, &["install", package]) { + panic!("Failed to install {}", package); + } + } + + install_packages(&package_list, &mut packages); save_packages_to_path(&packages, &file_path); let updated_packages = load_packages_from_path(&file_path); - assert_eq!(updated_packages.packages.get("pandas").unwrap(), "latest"); + + for package in &package_list { + assert_eq!(updated_packages.packages.get(package).unwrap(), "latest"); + } } #[test] fn test_delete_package() { let dir = tempdir().expect("Failed to create temp dir"); + let env_path = dir.path().join("env"); + create_virtualenv(&env_path); + let file_path = dir.path().join("requirements.json"); let mut packages = PackageRegistry::new(); packages.packages.insert("pandas".to_string(), "1.0.0".to_string()); save_packages_to_path(&packages, &file_path); - delete_package("pandas", &mut packages); - save_packages_to_path(&packages, &file_path); - let updated_packages = load_packages_from_path(&file_path); - assert!(updated_packages.packages.get("pandas").is_none()); + + if run_command_in_virtualenv(&env_path, &["uninstall", "-y", "pandas"]) { + delete_package("pandas", &mut packages); + save_packages_to_path(&packages, &file_path); + let updated_packages = load_packages_from_path(&file_path); + assert!(updated_packages.packages.get("pandas").is_none()); + } else { + panic!("Failed to uninstall pandas"); + } } #[test] fn test_update_package() { let dir = tempdir().expect("Failed to create temp dir"); + let env_path = dir.path().join("env"); + create_virtualenv(&env_path); + let file_path = dir.path().join("requirements.json"); let mut packages = PackageRegistry::new(); packages.packages.insert("pandas".to_string(), "1.0.0".to_string()); save_packages_to_path(&packages, &file_path); - update_package("pandas", "2.0.0", &mut packages); - save_packages_to_path(&packages, &file_path); - let updated_packages = load_packages_from_path(&file_path); - assert_eq!(updated_packages.packages.get("pandas").unwrap(), "2.0.0"); + + if run_command_in_virtualenv(&env_path, &["install", "pandas==2.0.0"]) { + update_package("pandas", "2.0.0", &mut packages); + save_packages_to_path(&packages, &file_path); + let updated_packages = load_packages_from_path(&file_path); + assert_eq!(updated_packages.packages.get("pandas").unwrap(), "2.0.0"); + } else { + panic!("Failed to update pandas"); + } } #[test] @@ -84,3 +195,27 @@ fn test_list_packages() { save_packages_to_path(&packages, &file_path); list_packages(&packages); // This just prints to stdout, so we're testing for no panic } + +#[test] +fn test_install_from_requirements() { + let dir = tempdir().expect("Failed to create temp dir"); + let env_path = dir.path().join("env"); + create_virtualenv(&env_path); + + let file_path = dir.path().join("requirements.json"); + let mut initial_packages = PackageRegistry::new(); + initial_packages.packages.insert("pandas".to_string(), "1.0.0".to_string()); + initial_packages.packages.insert("numpy".to_string(), "1.19.5".to_string()); + save_packages_to_path(&initial_packages, &file_path); + + println!("Content of requirements.json: {}", fs::read_to_string(&file_path).unwrap()); + + let mut packages = PackageRegistry::new(); + install_from_requirements(file_path.to_str().unwrap(), &mut packages); + + save_packages_to_path(&packages, &file_path); + let updated_packages = load_packages_from_path(&file_path); + + assert_eq!(updated_packages.packages.get("pandas").unwrap(), "1.0.0"); + assert_eq!(updated_packages.packages.get("numpy").unwrap(), "1.19.5"); +}