diff --git a/Cargo.lock b/Cargo.lock index c3141f0..d9451d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -138,6 +138,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.8.0" @@ -146,9 +152,9 @@ checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "cc" -version = "1.1.37" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40545c26d092346d8a8dab71ee48e7685a7a9cba76e634790c215b41a4a7b4cf" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" dependencies = [ "shlex", ] @@ -159,20 +165,26 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "clap" -version = "4.5.20" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.20" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" dependencies = [ "anstream", "anstyle", @@ -182,9 +194,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" [[package]] name = "colorchoice" @@ -201,7 +213,7 @@ dependencies = [ "encode_unicode", "lazy_static", "libc", - "unicode-width", + "unicode-width 0.1.14", "windows-sys 0.52.0", ] @@ -462,9 +474,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.34" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "miniz_oxide", @@ -567,8 +579,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -663,6 +677,24 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + [[package]] name = "hyper-tls" version = "0.6.0" @@ -849,24 +881,15 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.17.8" +version = "0.17.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3" +checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281" dependencies = [ "console", - "instant", "number_prefix", "portable-atomic", - "unicode-width", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", + "unicode-width 0.2.0", + "web-time", ] [[package]] @@ -1046,15 +1069,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" -[[package]] -name = "openssl-src" -version = "300.4.0+3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a709e02f2b4aca747929cca5ed248880847c650233cf8b8cdc48f40aaf4898a6" -dependencies = [ - "cc", -] - [[package]] name = "openssl-sys" version = "0.9.104" @@ -1063,7 +1077,6 @@ checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ "cc", "libc", - "openssl-src", "pkg-config", "vcpkg", ] @@ -1120,6 +1133,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.89" @@ -1138,6 +1160,58 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.3", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" +dependencies = [ + "bytes", + "getrandom", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.3", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a626c6807713b15cac82a6acaccd6043c9a5408c24baae07611fec3f243da" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.37" @@ -1147,6 +1221,27 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + [[package]] name = "rand_core" version = "0.6.4" @@ -1173,7 +1268,7 @@ checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", "libredox", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -1220,6 +1315,7 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "hyper-rustls", "hyper-tls", "hyper-util", "ipnet", @@ -1230,27 +1326,53 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "quinn", + "rustls", "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", "tokio-native-tls", + "tokio-rustls", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", "windows-registry", ] +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-demangle" version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + [[package]] name = "rustc_version" version = "0.4.1" @@ -1273,6 +1395,20 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.23.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pemfile" version = "2.2.0" @@ -1287,6 +1423,20 @@ name = "rustls-pki-types" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +dependencies = [ + "web-time", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] [[package]] name = "ryu" @@ -1369,18 +1519,18 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.214" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", @@ -1472,6 +1622,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "spki" version = "0.7.3" @@ -1561,7 +1717,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +dependencies = [ + "thiserror-impl 2.0.3", ] [[package]] @@ -1575,6 +1740,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "time" version = "0.3.36" @@ -1604,6 +1780,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.41.1" @@ -1629,6 +1820,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + [[package]] name = "toml" version = "0.8.19" @@ -1712,6 +1914,18 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.3" @@ -1755,8 +1969,6 @@ dependencies = [ "dirs", "env_logger", "log", - "openssl", - "regex", "self_update", "serde", "serde_json", @@ -1869,6 +2081,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "which" version = "7.0.0" @@ -2121,6 +2352,27 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zerofrom" version = "0.1.4" @@ -2182,7 +2434,7 @@ dependencies = [ "displaydoc", "indexmap", "memchr", - "thiserror", + "thiserror 1.0.69", "time", ] @@ -2194,5 +2446,5 @@ checksum = "6413a546ada9dbcd0b9a3e0b0880581279e35047bce9797e523b3408e1df607c" dependencies = [ "base64", "ed25519-dalek", - "thiserror", + "thiserror 1.0.69", ] diff --git a/Cargo.toml b/Cargo.toml index fe57452..e5d9c35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,13 +14,11 @@ clap = "4.5.20" dirs = "5.0.1" env_logger = "0.11.5" log = "0.4.22" -self_update = { version = "0.41.0", features = ["archive-tar", "archive-zip", "compression-flate2"] } +self_update = { version = "0.41.0", features = ["archive-tar", "archive-zip", "compression-flate2", "rustls"] } serde = { version = "1.0.214", features = ["derive"] } serde_json = "1.0.132" toml = "0.8.19" which = "7.0.0" -openssl = { version = "0.10", features = ["vendored"] } -regex = "1.11.1" [dev-dependencies] tempfile = "3.14.0" diff --git a/README.md b/README.md index 51230ad..5c6f12b 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,27 @@ # UV Migrator -UV Migrator is a Rust-based tool designed to facilitate the migration of Python projects from various dependency management systems (like pip or poetry) to the UV package manager. This tool automates the process of creating a new UV-based project structure while preserving existing dependencies. +UV Migrator is a Rust-based tool designed to facilitate the migration of Python projects from various dependency +management systems (like pip or poetry) to the UV package manager. This tool automates the process of creating a new +UV-based project structure while preserving existing dependencies. + +## Disclaimer + +This project is not associated with astral or the uv project in anyway, please use at your own risk and keep backups of +your dependency declarations for reference ## Features - Supports migration from Poetry and PEP 621 project structures - Creates a new virtual environment using UV -- Automatically transfers dependencies from the existing `pyproject.toml` or `requirements.txt` to the new UV-based project +- Automatically transfers dependencies from the existing `pyproject.toml` or `requirements.txt` to the new UV-based + project - Attempts to migrate all [tool.*] configs to the new `pyproject.toml` file - Handles both main and development dependencies - Provides detailed logging for transparency and debugging - Supports importing extra index URLs from global pip configuration - Allows specifying additional index URLs during migration -- By default doesnt pin the python version via a .python-versions file incase the user uses asdf/mise and .tool-versions files +- By default, doesn't pin the python version via a .python-versions file in case the user uses asdf/mise and + .tool-versions files ## Prerequisites diff --git a/src/migrators/dependency.rs b/src/migrators/dependency.rs index 2d8024d..716800f 100644 --- a/src/migrators/dependency.rs +++ b/src/migrators/dependency.rs @@ -1,7 +1,9 @@ -#[derive(Debug, PartialEq, Clone)] +// migrators/dependency.rs +#[derive(Debug, Hash, Eq, PartialEq, Clone)] pub enum DependencyType { Main, Dev, + Group(String), } #[derive(Debug)] @@ -10,4 +12,4 @@ pub struct Dependency { pub version: Option, pub dep_type: DependencyType, pub environment_markers: Option, -} +} \ No newline at end of file diff --git a/src/migrators/mod.rs b/src/migrators/mod.rs index 7863157..ec83f3f 100644 --- a/src/migrators/mod.rs +++ b/src/migrators/mod.rs @@ -6,8 +6,9 @@ pub mod requirements; use crate::utils::{create_virtual_environment, parse_pip_conf, update_pyproject_toml}; pub use dependency::{Dependency, DependencyType}; -pub use detect::detect_project_type; +pub use detect::{detect_project_type, ProjectType}; use log::info; +use std::collections::HashMap; use std::fs; use std::path::Path; @@ -64,21 +65,31 @@ impl MigrationTool for UvTool { let uv_path = which::which("uv").map_err(|e| format!("Failed to find uv command: {}", e))?; - for dep_type in &[DependencyType::Main, DependencyType::Dev] { - let deps: Vec<_> = dependencies - .iter() - .filter(|d| d.dep_type == *dep_type) - .collect(); + // Group dependencies by type + let mut grouped_deps: HashMap<&DependencyType, Vec<&Dependency>> = HashMap::new(); + for dep in dependencies { + grouped_deps.entry(&dep.dep_type).or_default().push(dep); + } + // Process each group + for (dep_type, deps) in grouped_deps { if deps.is_empty() { continue; } let mut command = std::process::Command::new(&uv_path); command.arg("add"); - if *dep_type == DependencyType::Dev { - command.arg("--dev"); + + match dep_type { + DependencyType::Dev => { + command.arg("--dev"); + } + DependencyType::Group(group_name) => { + command.arg("--group").arg(group_name); + } + DependencyType::Main => {} // No special flag needed } + command.current_dir(project_dir); for dep in deps { @@ -108,7 +119,10 @@ impl MigrationTool for UvTool { command.arg(dep_str); } - info!("Running uv add command with dependencies: {:?}", command); + info!( + "Running uv add command for {:?} dependencies: {:?}", + dep_type, command + ); let output = command .output() .map_err(|e| format!("Failed to execute uv command: {}", e))?; @@ -138,8 +152,8 @@ pub fn run_migration( info!("Detected project type: {:?}", project_type); let migration_source: Box = match project_type { - detect::ProjectType::Poetry => Box::new(poetry::PoetryMigrationSource), - detect::ProjectType::Requirements => Box::new(requirements::RequirementsMigrationSource), + ProjectType::Poetry => Box::new(poetry::PoetryMigrationSource), + ProjectType::Requirements => Box::new(requirements::RequirementsMigrationSource), }; let dependencies = migration_source.extract_dependencies(project_dir)?; diff --git a/src/migrators/poetry.rs b/src/migrators/poetry.rs index aaa77b7..1d93563 100644 --- a/src/migrators/poetry.rs +++ b/src/migrators/poetry.rs @@ -1,3 +1,5 @@ +// In migrators/poetry.rs + use super::{MigrationSource, Dependency, DependencyType}; use crate::types::PyProject; use std::path::Path; @@ -27,11 +29,17 @@ impl MigrationSource for PoetryMigrationSource { } } - // Handle dev dependencies + // Handle group dependencies if let Some(groups) = &poetry.group { - for group in groups.values() { + for (group_name, group) in groups { + // Determine dependency type based on group name + let dep_type = match group_name.as_str() { + "dev" | "test" => DependencyType::Dev, + _ => DependencyType::Group(group_name.clone()) + }; + for (name, value) in &group.dependencies { - if let Some(dep) = self.format_dependency(name, value, DependencyType::Dev) { + if let Some(dep) = self.format_dependency(name, value, dep_type.clone()) { dependencies.push(dep); } } diff --git a/src/migrators/requirements.rs b/src/migrators/requirements.rs index c0607d9..e1f315f 100644 --- a/src/migrators/requirements.rs +++ b/src/migrators/requirements.rs @@ -1,4 +1,5 @@ -use super::{Dependency, DependencyType, MigrationSource}; +use super::dependency::DependencyType; +use super::{Dependency, MigrationSource}; use log::{debug, info}; use std::fs; use std::path::{Path, PathBuf}; @@ -7,28 +8,17 @@ pub struct RequirementsMigrationSource; impl MigrationSource for RequirementsMigrationSource { fn extract_dependencies(&self, project_dir: &Path) -> Result, String> { - let (main_requirements, dev_requirements) = self.find_requirements_files(project_dir); - - if main_requirements.is_none() && dev_requirements.is_empty() { + let requirements_files = self.find_requirements_files(project_dir); + if requirements_files.is_empty() { return Err("No requirements files found.".to_string()); } let mut dependencies = Vec::new(); - - // Process main requirements (requirements.txt) - if let Some(main_file) = main_requirements { - info!("Processing main requirements file: {}", main_file.display()); - let main_deps = self.process_requirements_file(&main_file, DependencyType::Main)?; - debug!("Extracted {} main dependencies", main_deps.len()); - dependencies.extend(main_deps); - } - - // Process dev requirements (requirements-*.txt) - for dev_file in dev_requirements { - info!("Processing dev requirements file: {}", dev_file.display()); - let dev_deps = self.process_requirements_file(&dev_file, DependencyType::Dev)?; - debug!("Extracted {} dev dependencies", dev_deps.len()); - dependencies.extend(dev_deps); + for (file_path, dep_type) in requirements_files { + info!("Processing requirements file: {}", file_path.display()); + let deps = self.process_requirements_file(&file_path, dep_type)?; + debug!("Extracted {} dependencies", deps.len()); + dependencies.extend(deps); } debug!("Total dependencies extracted: {}", dependencies.len()); @@ -37,9 +27,8 @@ impl MigrationSource for RequirementsMigrationSource { } impl RequirementsMigrationSource { - fn find_requirements_files(&self, dir: &Path) -> (Option, Vec) { - let mut main_requirements = None; - let mut dev_requirements = Vec::new(); + fn find_requirements_files(&self, dir: &Path) -> Vec<(PathBuf, DependencyType)> { + let mut requirements_files = Vec::new(); if let Ok(entries) = fs::read_dir(dir) { for entry in entries.filter_map(Result::ok) { @@ -47,20 +36,31 @@ impl RequirementsMigrationSource { if path.is_file() { if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) { if file_name == "requirements.txt" { - main_requirements = Some(path.clone()); + requirements_files.push((path.clone(), DependencyType::Main)); info!("Found main requirements file: {}", path.display()); } else if file_name.starts_with("requirements-") && file_name.ends_with(".txt") { - dev_requirements.push(path.clone()); - info!("Found dev requirements file: {}", path.display()); + let group_name = file_name + .strip_prefix("requirements-") + .unwrap() + .strip_suffix(".txt") + .unwrap(); + + let dep_type = match group_name { + "dev" => DependencyType::Dev, + _ => DependencyType::Group(group_name.to_string()), + }; + + requirements_files.push((path.clone(), dep_type)); + info!("Found {} requirements file: {}", group_name, path.display()); } } } } } - (main_requirements, dev_requirements) + requirements_files } fn process_requirements_file( diff --git a/tests/poetry_test.rs b/tests/poetry_test.rs index 40caf2b..15bdd24 100644 --- a/tests/poetry_test.rs +++ b/tests/poetry_test.rs @@ -123,11 +123,30 @@ mypy = "^1.11.1" assert_eq!(dependencies.len(), 4); // 1 main + 1 dev + 2 code-quality + // Check main dependencies + let main_deps: Vec<_> = dependencies + .iter() + .filter(|d| matches!(d.dep_type, DependencyType::Main)) + .collect(); + assert_eq!(main_deps.len(), 1); + assert_eq!(main_deps[0].name, "fastapi"); + + // Check dev dependencies let dev_deps: Vec<_> = dependencies .iter() - .filter(|d| d.dep_type == DependencyType::Dev) + .filter(|d| matches!(d.dep_type, DependencyType::Dev)) .collect(); - assert_eq!(dev_deps.len(), 3); // All non-main deps should be marked as Dev + assert_eq!(dev_deps.len(), 1); + assert_eq!(dev_deps[0].name, "pytest"); + + // Check code-quality group dependencies + let code_quality_deps: Vec<_> = dependencies + .iter() + .filter(|d| matches!(d.dep_type, DependencyType::Group(ref g) if g == "code-quality")) + .collect(); + assert_eq!(code_quality_deps.len(), 2); + assert!(code_quality_deps.iter().any(|d| d.name == "ruff")); + assert!(code_quality_deps.iter().any(|d| d.name == "mypy")); } /// Test handling of dependencies with extras (optional features) in a Poetry project. @@ -250,11 +269,9 @@ blue = ">=0.9.1" assert_eq!(dependencies.len(), 1); - let blue_dep = dependencies.iter() - .find(|d| d.name == "blue") - .unwrap(); + let blue_dep = dependencies.iter().find(|d| d.name == "blue").unwrap(); assert_eq!(blue_dep.version, Some(">=0.9.1".to_string())); - assert_eq!(blue_dep.dep_type, DependencyType::Dev); // All group dependencies should be marked as Dev + assert_eq!(blue_dep.dep_type, DependencyType::Dev); // All group dependencies should be marked as Dev } #[cfg(test)] @@ -294,10 +311,8 @@ description = "a module for parsing battlescribe rosters and allowing them to be authors = ["Test Author "] license = "MIT" "#; - fs::write( - test_dir.path().join("old.pyproject.toml"), - old_content, - ).map_err(|e| format!("Failed to write old.pyproject.toml: {}", e))?; + fs::write(test_dir.path().join("old.pyproject.toml"), old_content) + .map_err(|e| format!("Failed to write old.pyproject.toml: {}", e))?; // Create new pyproject.toml with default content let new_content = r#"[project] @@ -305,10 +320,8 @@ name = "test-project" version = "0.1.0" description = "Add your description here" "#; - fs::write( - test_dir.path().join("pyproject.toml"), - new_content, - ).map_err(|e| format!("Failed to write pyproject.toml: {}", e))?; + fs::write(test_dir.path().join("pyproject.toml"), new_content) + .map_err(|e| format!("Failed to write pyproject.toml: {}", e))?; // Run the migration update_pyproject_toml(test_dir.path(), &[])?; @@ -318,7 +331,10 @@ description = "Add your description here" .map_err(|e| format!("Failed to read result: {}", e))?; // Verify the changes - assert!(result.contains(r#"version = "1.3.0""#), "Version was not updated correctly"); + assert!( + result.contains(r#"version = "1.3.0""#), + "Version was not updated correctly" + ); assert!(result.contains(r#"description = "a module for parsing battlescribe rosters and allowing them to be printed or displayed cleanly""#), "Description was not updated correctly"); @@ -346,10 +362,8 @@ version = "1.3.0" authors = ["Test Author "] license = "MIT" "#; - fs::write( - test_dir.path().join("old.pyproject.toml"), - old_content, - ).map_err(|e| format!("Failed to write old.pyproject.toml: {}", e))?; + fs::write(test_dir.path().join("old.pyproject.toml"), old_content) + .map_err(|e| format!("Failed to write old.pyproject.toml: {}", e))?; // Create new pyproject.toml with default content let new_content = r#"[project] @@ -357,10 +371,8 @@ name = "test-project" version = "0.1.0" description = "Add your description here" "#; - fs::write( - test_dir.path().join("pyproject.toml"), - new_content, - ).map_err(|e| format!("Failed to write pyproject.toml: {}", e))?; + fs::write(test_dir.path().join("pyproject.toml"), new_content) + .map_err(|e| format!("Failed to write pyproject.toml: {}", e))?; // Run the migration update_pyproject_toml(test_dir.path(), &[])?; @@ -370,9 +382,14 @@ description = "Add your description here" .map_err(|e| format!("Failed to read result: {}", e))?; // Verify the changes - assert!(result.contains(r#"version = "1.3.0""#), "Version was not updated correctly"); - assert!(result.contains(r#"description = "Add your description here""#), - "Description should remain unchanged when not present in Poetry file"); + assert!( + result.contains(r#"version = "1.3.0""#), + "Version was not updated correctly" + ); + assert!( + result.contains(r#"description = "Add your description here""#), + "Description should remain unchanged when not present in Poetry file" + ); Ok(()) } @@ -397,10 +414,8 @@ name = "test-project" version = "0.1.0" description = "Add your description here" "#; - fs::write( - test_dir.path().join("pyproject.toml"), - new_content, - ).map_err(|e| format!("Failed to write pyproject.toml: {}", e))?; + fs::write(test_dir.path().join("pyproject.toml"), new_content) + .map_err(|e| format!("Failed to write pyproject.toml: {}", e))?; // Run the migration update_pyproject_toml(test_dir.path(), &[])?; @@ -410,8 +425,11 @@ description = "Add your description here" .map_err(|e| format!("Failed to read result: {}", e))?; // Verify nothing changed - assert_eq!(result, new_content, "File should remain unchanged when no old.pyproject.toml exists"); + assert_eq!( + result, new_content, + "File should remain unchanged when no old.pyproject.toml exists" + ); Ok(()) } -} \ No newline at end of file +} diff --git a/tests/requirements_text_test.rs b/tests/requirements_text_test.rs index 0a6847b..9f6ba5c 100644 --- a/tests/requirements_text_test.rs +++ b/tests/requirements_text_test.rs @@ -84,12 +84,13 @@ requests==2.31.0 # HTTP client assert_eq!(dependencies.len(), 3); } -/// Test handling of multiple requirements files (main and dev requirements). +/// Test handling of multiple requirements files (main, dev, and test requirements). /// /// This test verifies that: -/// 1. Both main and dev requirements are processed -/// 2. Dependencies are correctly categorized as main or dev -/// 3. All files are properly parsed +/// 1. Requirements from all files are processed +/// 2. Dependencies are correctly categorized into main, dev, and test groups +/// 3. The correct number of dependencies are found in each category +/// 4. Specific packages are found in their expected categories #[test] fn test_multiple_requirements_files() { let main_content = "flask==2.0.0\nrequests==2.31.0"; @@ -105,19 +106,80 @@ fn test_multiple_requirements_files() { let source = RequirementsMigrationSource; let dependencies = source.extract_dependencies(&project_dir).unwrap(); - assert_eq!(dependencies.len(), 6); + // Verify total number of dependencies + assert_eq!( + dependencies.len(), + 6, + "Total number of dependencies should be 6" + ); + // Check main dependencies let main_deps: Vec<_> = dependencies .iter() - .filter(|d| d.dep_type == DependencyType::Main) + .filter(|d| matches!(d.dep_type, DependencyType::Main)) .collect(); - assert_eq!(main_deps.len(), 2); + assert_eq!(main_deps.len(), 2, "Should have 2 main dependencies"); + assert!( + main_deps.iter().any(|d| d.name == "flask"), + "Main dependencies should include flask" + ); + assert!( + main_deps.iter().any(|d| d.name == "requests"), + "Main dependencies should include requests" + ); + // Check dev dependencies let dev_deps: Vec<_> = dependencies .iter() - .filter(|d| d.dep_type == DependencyType::Dev) + .filter(|d| matches!(d.dep_type, DependencyType::Dev)) .collect(); - assert_eq!(dev_deps.len(), 4); + assert_eq!(dev_deps.len(), 2, "Should have 2 dev dependencies"); + assert!( + dev_deps.iter().any(|d| d.name == "pytest"), + "Dev dependencies should include pytest" + ); + assert!( + dev_deps.iter().any(|d| d.name == "black"), + "Dev dependencies should include black" + ); + + // Check test dependencies + let test_deps: Vec<_> = dependencies + .iter() + .filter(|d| matches!(d.dep_type, DependencyType::Group(ref g) if g == "test")) + .collect(); + assert_eq!(test_deps.len(), 2, "Should have 2 test dependencies"); + assert!( + test_deps.iter().any(|d| d.name == "pytest-cov"), + "Test dependencies should include pytest-cov" + ); + assert!( + test_deps.iter().any(|d| d.name == "pytest-mock"), + "Test dependencies should include pytest-mock" + ); + + // Verify versions for a sample of dependencies + if let Some(flask_dep) = dependencies.iter().find(|d| d.name == "flask") { + assert_eq!( + flask_dep.version, + Some("2.0.0".to_string()), + "Flask version should be 2.0.0" + ); + } + if let Some(pytest_dep) = dependencies.iter().find(|d| d.name == "pytest") { + assert_eq!( + pytest_dep.version, + Some("7.0.0".to_string()), + "Pytest version should be 7.0.0" + ); + } + if let Some(pytest_cov_dep) = dependencies.iter().find(|d| d.name == "pytest-cov") { + assert_eq!( + pytest_cov_dep.version, + Some("4.1.0".to_string()), + "Pytest-cov version should be 4.1.0" + ); + } } /// Test handling of environment markers in requirements.