Skip to content

Commit

Permalink
feat(tests): requirements.txt and poetry parser and migration tests, …
Browse files Browse the repository at this point in the history
…these should have been in from the getgo but will also work as a better frame of reference for other tools
  • Loading branch information
stvnksslr committed Nov 10, 2024
1 parent a51dcb7 commit d503139
Show file tree
Hide file tree
Showing 7 changed files with 680 additions and 31 deletions.
30 changes: 28 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@ serde = { version = "1.0.210" , features = ["derive"] }
serde_json = "1.0.132"
toml = "0.8.19"
which = "6.0.3"

[dev-dependencies]
tempfile = "3.10.1"
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod migrator;
pub mod types;
pub mod utils;
4 changes: 2 additions & 2 deletions src/migrator/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
mod dependency;
mod detect;
mod poetry;
pub mod poetry;
mod pyproject;
mod requirements;
pub mod requirements;

use crate::utils::{create_virtual_environment, parse_pip_conf, update_pyproject_toml};
pub use dependency::{Dependency, DependencyType};
Expand Down
144 changes: 117 additions & 27 deletions src/migrator/requirements.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::{MigrationSource, Dependency, DependencyType};
use std::path::{Path, PathBuf};
use super::{Dependency, DependencyType, MigrationSource};
use log::{debug, info};
use std::fs;
use log::{info, debug};
use std::path::{Path, PathBuf};

pub struct RequirementsMigrationSource;

Expand All @@ -27,7 +27,7 @@ impl MigrationSource for RequirementsMigrationSource {
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 from {}", dev_deps.len(), dev_file.display());
debug!("Extracted {} dev dependencies", dev_deps.len());
dependencies.extend(dev_deps);
}

Expand All @@ -49,7 +49,9 @@ impl RequirementsMigrationSource {
if file_name == "requirements.txt" {
main_requirements = Some(path.clone());
info!("Found main requirements file: {}", path.display());
} else if file_name.starts_with("requirements-") && file_name.ends_with(".txt") {
} else if file_name.starts_with("requirements-")
&& file_name.ends_with(".txt")
{
dev_requirements.push(path.clone());
info!("Found dev requirements file: {}", path.display());
}
Expand All @@ -61,62 +63,150 @@ impl RequirementsMigrationSource {
(main_requirements, dev_requirements)
}

fn process_requirements_file(&self, file_path: &Path, dep_type: DependencyType) -> Result<Vec<Dependency>, String> {
fn process_requirements_file(
&self,
file_path: &Path,
dep_type: DependencyType,
) -> Result<Vec<Dependency>, String> {
let contents = fs::read_to_string(file_path)
.map_err(|e| format!("Error reading file '{}': {}", file_path.display(), e))?;

let mut dependencies = Vec::new();

for (line_num, line) in contents.lines().enumerate() {
let line = line.trim();

// Skip empty lines and comments
if line.is_empty() || line.starts_with('#') {
continue;
}

match self.parse_requirement(line, dep_type.clone()) {
Some(dep) => {
match self.parse_requirement(line) {
Ok(Some(dep)) => {
debug!("Parsed dependency on line {}: {:?}", line_num + 1, dep);
dependencies.push(dep);
},
None => debug!("Skipped line {} (possibly 'python' requirement): {}", line_num + 1, line),
dependencies.push(Dependency {
name: dep.name,
version: dep.version,
dep_type: dep_type.clone(),
environment_markers: dep.environment_markers,
});
}
Ok(None) => debug!(
"Skipped line {} (possibly 'python' requirement): {}",
line_num + 1,
line
),
Err(e) => debug!("Failed to parse line {}: {}", line_num + 1, e),
}
}

debug!("Processed {} with {} dependencies", file_path.display(), dependencies.len());
debug!("Processed {} dependencies", dependencies.len());
Ok(dependencies)
}

fn parse_requirement(&self, line: &str, dep_type: DependencyType) -> Option<Dependency> {
fn process_version_spec(&self, version_spec: &str) -> String {
let version_spec = version_spec.trim();
if version_spec.starts_with("==") {
version_spec[2..].to_string()
} else if version_spec.starts_with(">=") || version_spec.starts_with("<=") {
version_spec[2..].to_string()
} else if version_spec.starts_with('>') || version_spec.starts_with('<') {
version_spec[1..].to_string()
} else {
version_spec.to_string()
}
}

fn parse_requirement(&self, line: &str) -> Result<Option<Dependency>, String> {
// Handle editable installs
let line = if line.starts_with("-e") {
let parts: Vec<&str> = line.splitn(2, ' ').collect();
if parts.len() != 2 {
return Err("Invalid editable install format".to_string());
}
parts[1]
} else {
line
};

// Split the line into package specification and environment markers
let parts: Vec<&str> = line.split(';').collect();
let package_spec = parts[0].trim();

// Parse package name and version
let pkg_parts: Vec<&str> = package_spec.split(&['=', '>', '<', '~', '!'][..]).collect();
let name = pkg_parts[0].trim().to_string();
// Handle malformed lines
if package_spec.is_empty() || package_spec.contains("===") {
return Err("Malformed requirement line".to_string());
}

// Handle URLs and git repositories
let (name, version) =
if package_spec.starts_with("git+") || package_spec.starts_with("http") {
let name = if let Some(egg_part) = package_spec.split('#').last() {
if egg_part.starts_with("egg=") {
egg_part.trim_start_matches("egg=")
} else if package_spec.ends_with(".whl") {
package_spec
.split('/')
.last()
.and_then(|f| f.split('-').next())
.ok_or("Invalid wheel filename")?
} else {
return Err("Invalid URL format".to_string());
}
} else {
package_spec
.split('/')
.last()
.and_then(|f| f.split('.').next())
.ok_or("Invalid URL format")?
};
(name.to_string(), None)
} else {
// Regular package specification
let (name, version) = {
if !package_spec.contains(&['>', '<', '=', '~', '!'][..]) {
(package_spec.to_string(), None)
} else {
let name_end = package_spec
.find(|c| ['>', '<', '=', '~', '!'].contains(&c))
.unwrap();
let name = package_spec[..name_end].trim().to_string();
let version_spec = package_spec[name_end..].trim();

let version = if version_spec.contains(',') {
// For multiple version constraints
let version_parts: Vec<String> = version_spec
.split(',')
.map(|p| p.trim().to_string())
.collect();
Some(version_parts.join(","))
} else {
// For single version constraint
Some(self.process_version_spec(version_spec))
};

(name, version)
}
};
(name, version)
};

if name == "python" {
return None;
return Ok(None);
}

let version = if pkg_parts.len() > 1 {
Some(pkg_parts[1..].join("").trim().to_string())
} else {
None
};

// Handle environment markers
let environment_markers = if parts.len() > 1 {
Some(parts[1..].join(";").trim().to_string())
} else {
None
};

Some(Dependency {
Ok(Some(Dependency {
name,
version,
dep_type,
dep_type: DependencyType::Main, // This will be overridden by the caller
environment_markers,
})
}))
}
}
}
Loading

0 comments on commit d503139

Please sign in to comment.