Skip to content

Commit

Permalink
feat(dependency groups): dependency groups are now properly supported…
Browse files Browse the repository at this point in the history
… by uv, if you are using some form of grouping requirements-test.txt or poetry [tool.poetry.group.test.dependencies] these will now carry over, if you are not using them everything will fall under --dev as normal
  • Loading branch information
stvnksslr committed Nov 15, 2024
1 parent 3d21730 commit 0f814c9
Show file tree
Hide file tree
Showing 9 changed files with 496 additions and 133 deletions.
340 changes: 296 additions & 44 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
6 changes: 4 additions & 2 deletions src/migrators/dependency.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -10,4 +12,4 @@ pub struct Dependency {
pub version: Option<String>,
pub dep_type: DependencyType,
pub environment_markers: Option<String>,
}
}
36 changes: 25 additions & 11 deletions src/migrators/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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))?;
Expand Down Expand Up @@ -138,8 +152,8 @@ pub fn run_migration(
info!("Detected project type: {:?}", project_type);

let migration_source: Box<dyn MigrationSource> = 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)?;
Expand Down
14 changes: 11 additions & 3 deletions src/migrators/poetry.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// In migrators/poetry.rs

use super::{MigrationSource, Dependency, DependencyType};
use crate::types::PyProject;
use std::path::Path;
Expand Down Expand Up @@ -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);
}
}
Expand Down
52 changes: 26 additions & 26 deletions src/migrators/requirements.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -7,28 +8,17 @@ pub struct RequirementsMigrationSource;

impl MigrationSource for RequirementsMigrationSource {
fn extract_dependencies(&self, project_dir: &Path) -> Result<Vec<Dependency>, 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());
Expand All @@ -37,30 +27,40 @@ impl MigrationSource for RequirementsMigrationSource {
}

impl RequirementsMigrationSource {
fn find_requirements_files(&self, dir: &Path) -> (Option<PathBuf>, Vec<PathBuf>) {
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) {
let path = entry.path();
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(
Expand Down
Loading

0 comments on commit 0f814c9

Please sign in to comment.