From e13d3e74958ec3553e1d9a5ad6a99c4bc388bae1 Mon Sep 17 00:00:00 2001 From: Alexander Gherm Date: Sun, 20 Oct 2024 17:00:13 +0200 Subject: [PATCH 1/4] Fix to respect comments positioning in pyproject.toml on change --- crates/uv-workspace/src/pyproject_mut.rs | 68 ++++++++++++++++++++---- crates/uv/tests/it/edit.rs | 65 ++++++++++++++++++++++ 2 files changed, 123 insertions(+), 10 deletions(-) diff --git a/crates/uv-workspace/src/pyproject_mut.rs b/crates/uv-workspace/src/pyproject_mut.rs index 3befdd5ac20e..f6fa92ee0c36 100644 --- a/crates/uv-workspace/src/pyproject_mut.rs +++ b/crates/uv-workspace/src/pyproject_mut.rs @@ -49,6 +49,18 @@ pub enum ArrayEdit { Add(usize), } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum CommentType { + FullLine, + SameLine, +} + +#[derive(Debug, Clone)] +struct Comment { + text: String, + comment_type: CommentType, +} + impl ArrayEdit { pub fn index(&self) -> usize { match self { @@ -694,7 +706,12 @@ pub fn add_dependency( }; let index = index.unwrap_or(deps.len()); - deps.insert(index, req_string); + let mut value = Value::from(req_string.as_str()); + let decor = value.decor_mut(); + decor.set_prefix(deps.trailing().clone()); + deps.set_trailing(""); + + deps.insert_formatted(index, value); // `reformat_array_multiline` uses the indentation of the first dependency entry. // Therefore, we retrieve the indentation of the first dependency entry and apply it to // the new entry. Note that it is only necessary if the newly added dependency is going @@ -829,14 +846,38 @@ fn try_parse_requirement(req: &str) -> Option { /// Reformats a TOML array to multi line while trying to preserve all comments /// and move them around. This also formats the array to have a trailing comma. fn reformat_array_multiline(deps: &mut Array) { - fn find_comments(s: Option<&RawString>) -> impl Iterator { - s.and_then(|x| x.as_str()) + fn find_comments(s: Option<&RawString>) -> Box + '_> { + let iter = s + .and_then(|x| x.as_str()) .unwrap_or("") .lines() - .filter_map(|line| { - let line = line.trim(); - line.starts_with('#').then_some(line) - }) + .scan( + (false, false), + |(prev_line_was_empty, prev_line_was_comment), line| { + let trimmed_line = line.trim(); + if let Some(index) = trimmed_line.find('#') { + let comment_text = trimmed_line[index..].trim().to_string(); + let comment_type = if (*prev_line_was_empty) || (*prev_line_was_comment) { + CommentType::FullLine + } else { + CommentType::SameLine + }; + *prev_line_was_empty = trimmed_line.is_empty(); + *prev_line_was_comment = true; + Some(Some(Comment { + text: comment_text, + comment_type, + })) + } else { + *prev_line_was_empty = trimmed_line.is_empty(); + *prev_line_was_comment = false; + Some(None) + } + }, + ) + .filter_map(|x| x); + + Box::new(iter) } let mut indentation_prefix = None; @@ -866,8 +907,15 @@ fn reformat_array_multiline(deps: &mut Array) { let indentation_prefix_str = format!("\n{}", indentation_prefix.as_ref().unwrap()); for comment in find_comments(decor.prefix()).chain(find_comments(decor.suffix())) { - prefix.push_str(&indentation_prefix_str); - prefix.push_str(comment); + match comment.comment_type { + CommentType::FullLine => { + prefix.push_str(&indentation_prefix_str); + } + CommentType::SameLine => { + prefix.push_str(" "); + } + } + prefix.push_str(&comment.text); } prefix.push_str(&indentation_prefix_str); decor.set_prefix(prefix); @@ -880,7 +928,7 @@ fn reformat_array_multiline(deps: &mut Array) { if comments.peek().is_some() { for comment in comments { rv.push_str("\n "); - rv.push_str(comment); + rv.push_str(&comment.text); } } if !rv.is_empty() || !deps.is_empty() { diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index ffbdf0cc0be0..86caca32e154 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -6269,3 +6269,68 @@ fn add_self() -> Result<()> { Ok(()) } + +#[test] +fn add_preserves_comments_indentation_and_sameline_comments() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + # comment 0 + "anyio==3.7.0", # comment 1 + ] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#})?; + + uv_snapshot!(context.filters(), context.add().arg("requests==2.31.0"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 8 packages in [TIME] + Prepared 8 packages in [TIME] + Installed 8 packages in [TIME] + + anyio==3.7.0 + + certifi==2024.2.2 + + charset-normalizer==3.3.2 + + idna==3.6 + + project==0.1.0 (from file://[TEMP_DIR]/) + + requests==2.31.0 + + sniffio==1.3.1 + + urllib3==2.2.1 + "###); + + let pyproject_toml = context.read("pyproject.toml"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + # comment 0 + "anyio==3.7.0", # comment 1 + "requests==2.31.0", + ] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "### + ); + }); + Ok(()) +} From 501a5eb554c04b72486e1aad2240487b50e01a3a Mon Sep 17 00:00:00 2001 From: Alexander Gherm Date: Sun, 20 Oct 2024 17:15:07 +0200 Subject: [PATCH 2/4] Satisfy linter --- crates/uv-workspace/src/pyproject_mut.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/uv-workspace/src/pyproject_mut.rs b/crates/uv-workspace/src/pyproject_mut.rs index f6fa92ee0c36..7fa95f5687dc 100644 --- a/crates/uv-workspace/src/pyproject_mut.rs +++ b/crates/uv-workspace/src/pyproject_mut.rs @@ -875,7 +875,7 @@ fn reformat_array_multiline(deps: &mut Array) { } }, ) - .filter_map(|x| x); + .flatten(); Box::new(iter) } @@ -912,7 +912,7 @@ fn reformat_array_multiline(deps: &mut Array) { prefix.push_str(&indentation_prefix_str); } CommentType::SameLine => { - prefix.push_str(" "); + prefix.push(' '); } } prefix.push_str(&comment.text); From 5f3538071a958d8d3191672f61a2fc89a9478d02 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 20 Oct 2024 13:02:17 -0400 Subject: [PATCH 3/4] Rename enum variants --- crates/uv-workspace/src/pyproject_mut.rs | 14 ++++++++------ crates/uv/tests/it/edit.rs | 3 ++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/uv-workspace/src/pyproject_mut.rs b/crates/uv-workspace/src/pyproject_mut.rs index 7fa95f5687dc..696aa442fadd 100644 --- a/crates/uv-workspace/src/pyproject_mut.rs +++ b/crates/uv-workspace/src/pyproject_mut.rs @@ -51,8 +51,10 @@ pub enum ArrayEdit { #[derive(Debug, Copy, Clone, PartialEq, Eq)] enum CommentType { - FullLine, - SameLine, + /// A comment that appears on its own line. + OwnLine, + /// A comment that appears at the end of a line. + EndOfLine, } #[derive(Debug, Clone)] @@ -858,9 +860,9 @@ fn reformat_array_multiline(deps: &mut Array) { if let Some(index) = trimmed_line.find('#') { let comment_text = trimmed_line[index..].trim().to_string(); let comment_type = if (*prev_line_was_empty) || (*prev_line_was_comment) { - CommentType::FullLine + CommentType::OwnLine } else { - CommentType::SameLine + CommentType::EndOfLine }; *prev_line_was_empty = trimmed_line.is_empty(); *prev_line_was_comment = true; @@ -908,10 +910,10 @@ fn reformat_array_multiline(deps: &mut Array) { for comment in find_comments(decor.prefix()).chain(find_comments(decor.suffix())) { match comment.comment_type { - CommentType::FullLine => { + CommentType::OwnLine => { prefix.push_str(&indentation_prefix_str); } - CommentType::SameLine => { + CommentType::EndOfLine => { prefix.push(' '); } } diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 86caca32e154..4c4f3fdaadf3 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -6271,7 +6271,7 @@ fn add_self() -> Result<()> { } #[test] -fn add_preserves_comments_indentation_and_sameline_comments() -> Result<()> { +fn add_preserves_end_of_line_comments() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -6283,6 +6283,7 @@ fn add_preserves_comments_indentation_and_sameline_comments() -> Result<()> { dependencies = [ # comment 0 "anyio==3.7.0", # comment 1 + # comment 2 ] [build-system] From 30c1eb423026e0825c87b6ce17369473665fc332 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 20 Oct 2024 13:04:17 -0400 Subject: [PATCH 4/4] One more test --- crates/uv/tests/it/edit.rs | 68 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 4c4f3fdaadf3..94607c90477c 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -6324,6 +6324,7 @@ fn add_preserves_end_of_line_comments() -> Result<()> { dependencies = [ # comment 0 "anyio==3.7.0", # comment 1 + # comment 2 "requests==2.31.0", ] @@ -6335,3 +6336,70 @@ fn add_preserves_end_of_line_comments() -> Result<()> { }); Ok(()) } + +#[test] +fn add_preserves_open_bracket_comment() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ # comment 0 + # comment 1 + "anyio==3.7.0", # comment 2 + # comment 3 + ] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#})?; + + uv_snapshot!(context.filters(), context.add().arg("requests==2.31.0"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 8 packages in [TIME] + Prepared 8 packages in [TIME] + Installed 8 packages in [TIME] + + anyio==3.7.0 + + certifi==2024.2.2 + + charset-normalizer==3.3.2 + + idna==3.6 + + project==0.1.0 (from file://[TEMP_DIR]/) + + requests==2.31.0 + + sniffio==1.3.1 + + urllib3==2.2.1 + "###); + + let pyproject_toml = context.read("pyproject.toml"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ # comment 0 + # comment 1 + "anyio==3.7.0", # comment 2 + # comment 3 + "requests==2.31.0", + ] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "### + ); + }); + Ok(()) +}