Skip to content

Commit

Permalink
feat(toml parsing): moved all the toml edits / parsing to the toml-ed…
Browse files Browse the repository at this point in the history
…its package instead of grabbing specific lines
  • Loading branch information
stvnksslr committed Dec 22, 2024
1 parent d1356c1 commit 36ae110
Show file tree
Hide file tree
Showing 12 changed files with 726 additions and 605 deletions.
9 changes: 5 additions & 4 deletions Cargo.lock

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

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ self_update = { version = "0.41.0", features = [
"rustls",
], default-features = false, optional = true }
serde = { version = "1.0.214", features = ["derive"] }
serde_json = "1.0.132"
serde_json = "1.0.134"
toml = "0.8.19"
which = "7.0.1"
semver = "1.0"
toml_edit = "0.22.22"

[dev-dependencies]
tempfile = "3.14.0"
Expand Down
17 changes: 14 additions & 3 deletions src/migrators/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

use crate::migrators::detect::{PoetryProjectType, ProjectType};
use crate::utils::{
create_virtual_environment, parse_pip_conf, update_authors, update_pyproject_toml, FileTrackerGuard,
create_virtual_environment, parse_pip_conf, update_authors, update_pyproject_toml, update_url,
FileTrackerGuard,
};
use log::info;
use setup_py::SetupPyMigrationSource;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
Expand Down Expand Up @@ -240,7 +242,9 @@ pub fn run_migration(
pyproject::append_tool_sections(project_dir)?;

// Migrate setup.py metadata
if let Some(description) = setup_py::SetupPyMigrationSource::extract_description(project_dir)? {
if let Some(description) =
setup_py::SetupPyMigrationSource::extract_description(project_dir)?
{
info!("Migrating description from setup.py");
file_tracker.track_file(&pyproject_path)?;
crate::utils::pyproject::update_description(project_dir, &description)?;
Expand All @@ -251,6 +255,13 @@ pub fn run_migration(
file_tracker.track_file(&pyproject_path)?;
update_authors(project_dir)?;

// Migrate URL from setup.py
info!("Migrating URL from setup.py");
let url = SetupPyMigrationSource::extract_url(project_dir)?;
if let Some(project_url) = url {
update_url(project_dir, &project_url)?;
}

if hello_py_path.exists() {
fs::remove_file(&hello_py_path)
.map_err(|e| format!("Failed to delete hello.py: {}", e))?;
Expand Down Expand Up @@ -280,4 +291,4 @@ pub fn run_migration(
}

result
}
}
185 changes: 135 additions & 50 deletions src/migrators/poetry.rs
Original file line number Diff line number Diff line change
@@ -1,36 +1,31 @@
use super::{Dependency, DependencyType, MigrationSource};
use crate::migrators::detect::PoetryProjectType;
use crate::types::PyProject;
use crate::utils::toml::read_toml;
use log::{debug, info};
use std::fs;
use std::path::Path;
use toml_edit::{Item, Value};

pub struct PoetryMigrationSource;

impl PoetryMigrationSource {
pub fn detect_project_type(project_dir: &Path) -> Result<PoetryProjectType, String> {
let pyproject_path = project_dir.join("pyproject.toml");
let contents = fs::read_to_string(&pyproject_path)
.map_err(|e| format!("Error reading file '{}': {}", pyproject_path.display(), e))?;

let pyproject: PyProject = toml::from_str(&contents).map_err(|e| {
format!(
"Error parsing TOML in '{}': {}",
pyproject_path.display(),
e
)
})?;

if let Some(tool) = &pyproject.tool {
if let Some(poetry) = &tool.poetry {
let doc = read_toml(&pyproject_path)?;

if let Some(tool) = doc.get("tool") {
if let Some(poetry) = tool.get("poetry") {
// Check if packages configuration exists and includes "src"
let is_package = poetry.packages.as_ref().map_or(false, |packages| {
packages.iter().any(|pkg| {
pkg.include
.as_ref()
.map_or(false, |include| include == "src")
})
});
let is_package = poetry
.get("packages")
.and_then(|packages| packages.as_array())
.map_or(false, |packages| {
packages.iter().any(|pkg| {
pkg.as_inline_table()
.and_then(|t| t.get("include"))
.and_then(|i| i.as_str())
.map_or(false, |include| include == "src")
})
});

debug!(
"Poetry project type detected: {}",
Expand All @@ -45,13 +40,13 @@ impl PoetryMigrationSource {
}

debug!("No package configuration found, defaulting to application");
Ok(PoetryProjectType::Application) // Default to application if not explicitly configured
Ok(PoetryProjectType::Application)
}

fn format_dependency(
&self,
name: &str,
value: &toml::Value,
value: &Item,
dep_type: DependencyType,
) -> Option<Dependency> {
if name == "python" {
Expand All @@ -60,8 +55,8 @@ impl PoetryMigrationSource {
}

let version = match value {
toml::Value::String(v) => {
let v = v.trim();
Item::Value(Value::String(v)) => {
let v = v.value().trim();
if v == "*" {
debug!("Found wildcard version for {}, setting to None", name);
None
Expand All @@ -70,7 +65,7 @@ impl PoetryMigrationSource {
Some(v.to_string())
}
}
toml::Value::Table(t) => {
Item::Table(t) => {
let version = t
.get("version")
.and_then(|v| v.as_str())
Expand Down Expand Up @@ -103,25 +98,16 @@ impl MigrationSource for PoetryMigrationSource {
fn extract_dependencies(&self, project_dir: &Path) -> Result<Vec<Dependency>, String> {
info!("Extracting dependencies from Poetry project");
let pyproject_path = project_dir.join("pyproject.toml");
let contents = fs::read_to_string(&pyproject_path)
.map_err(|e| format!("Error reading file '{}': {}", pyproject_path.display(), e))?;

let pyproject: PyProject = toml::from_str(&contents).map_err(|e| {
format!(
"Error parsing TOML in '{}': {}",
pyproject_path.display(),
e
)
})?;
let doc = read_toml(&pyproject_path)?;

let mut dependencies = Vec::new();

if let Some(tool) = &pyproject.tool {
if let Some(poetry) = &tool.poetry {
if let Some(tool) = doc.get("tool") {
if let Some(poetry) = tool.get("poetry") {
// Handle main dependencies
if let Some(deps) = &poetry.dependencies {
if let Some(deps) = poetry.get("dependencies").and_then(|d| d.as_table()) {
debug!("Processing main dependencies");
for (name, value) in deps {
for (name, value) in deps.iter() {
if let Some(dep) = self.format_dependency(name, value, DependencyType::Main)
{
debug!("Added main dependency: {}", name);
Expand All @@ -131,20 +117,27 @@ impl MigrationSource for PoetryMigrationSource {
}

// Handle group dependencies
if let Some(groups) = &poetry.group {
if let Some(groups) = poetry.get("group").and_then(|g| g.as_table()) {
debug!("Processing group dependencies");
for (group_name, group) in groups {
let dep_type = match group_name.as_str() {
for (group_name, group) in groups.iter() {
let dep_type = match group_name {
"dev" => DependencyType::Dev,
_ => DependencyType::Group(group_name.clone()),
_ => DependencyType::Group(group_name.to_string()),
};
debug!("Processing group: {}", group_name);

for (name, value) in &group.dependencies {
if let Some(dep) = self.format_dependency(name, value, dep_type.clone())
{
debug!("Added {} dependency: {}", group_name, name);
dependencies.push(dep);
if let Some(deps) = group
.as_table()
.and_then(|g| g.get("dependencies"))
.and_then(|d| d.as_table())
{
for (name, value) in deps.iter() {
if let Some(dep) =
self.format_dependency(name, value, dep_type.clone())
{
debug!("Added {} dependency: {}", group_name, name);
dependencies.push(dep);
}
}
}
}
Expand All @@ -156,3 +149,95 @@ impl MigrationSource for PoetryMigrationSource {
Ok(dependencies)
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;

fn create_test_project(content: &str) -> (TempDir, std::path::PathBuf) {
let temp_dir = TempDir::new().unwrap();
let project_dir = temp_dir.path().to_path_buf();
let pyproject_path = project_dir.join("pyproject.toml");
fs::write(&pyproject_path, content).unwrap();
(temp_dir, project_dir)
}

#[test]
fn test_extract_main_dependencies() {
let content = r#"
[tool.poetry]
name = "test-project"
version = "0.1.0"
[tool.poetry.dependencies]
python = "^3.11"
fastapi = "^0.111.0"
aiofiles = "24.1.0"
jinja2 = { version = "^3.1.4" }
uvicorn = { extras = ["standard"], version = "^0.30.1" }
"#;
let (_temp_dir, project_dir) = create_test_project(content);

let source = PoetryMigrationSource;
let dependencies = source.extract_dependencies(&project_dir).unwrap();

assert_eq!(dependencies.len(), 4); // Should not include python

let fastapi_dep = dependencies.iter().find(|d| d.name == "fastapi").unwrap();
assert_eq!(fastapi_dep.version, Some("^0.111.0".to_string()));
assert_eq!(fastapi_dep.dep_type, DependencyType::Main);

let aiofiles_dep = dependencies.iter().find(|d| d.name == "aiofiles").unwrap();
assert_eq!(aiofiles_dep.version, Some("24.1.0".to_string()));
assert_eq!(aiofiles_dep.dep_type, DependencyType::Main);
}

#[test]
fn test_extract_dev_dependencies() {
let content = r#"
[tool.poetry]
name = "test-project"
version = "0.1.0"
[tool.poetry.group.dev.dependencies]
pytest = "^8.2.2"
pytest-cov = "^5.0.0"
pytest-sugar = "^1.0.0"
"#;
let (_temp_dir, project_dir) = create_test_project(content);

let source = PoetryMigrationSource;
let dependencies = source.extract_dependencies(&project_dir).unwrap();

assert_eq!(dependencies.len(), 3);
for dep in dependencies {
assert!(matches!(dep.dep_type, DependencyType::Dev));
}
}

#[test]
fn test_detect_project_type() {
let package_content = r#"
[tool.poetry]
name = "test-project"
version = "0.1.0"
[tool.poetry.packages]
include = "src"
"#;
let (_temp_dir, project_dir) = create_test_project(package_content);
let result = PoetryMigrationSource::detect_project_type(&project_dir).unwrap();
assert!(matches!(result, PoetryProjectType::Package));

let app_content = r#"
[tool.poetry]
name = "test-project"
version = "0.1.0"
"#;
let (_temp_dir, project_dir) = create_test_project(app_content);
let result = PoetryMigrationSource::detect_project_type(&project_dir).unwrap();
assert!(matches!(result, PoetryProjectType::Application));
}
}
Loading

0 comments on commit 36ae110

Please sign in to comment.