From 918ddef090b36da7bd3a3117e59ea1a657d29795 Mon Sep 17 00:00:00 2001 From: bnorick Date: Fri, 10 Jan 2025 17:07:03 -0800 Subject: [PATCH] Fixes bug in `uv remove` when only comments exist (#10484) ## Summary Fixes a bug when there are only comments in the dependencies section. Basically, after one removes all dependencies, if there are remaining comments then the value unwrapped here https://github.com/astral-sh/uv/blob/c198e2233efaa037a9154fd7fe2625e4c78d976b/crates/uv-workspace/src/pyproject_mut.rs#L1309 is never properly initialized. It's initialized to `None`, here https://github.com/astral-sh/uv/blob/c198e2233efaa037a9154fd7fe2625e4c78d976b/crates/uv-workspace/src/pyproject_mut.rs#L1256, but doesn't get set to `Some(...)` until the first dependency here https://github.com/astral-sh/uv/blob/c198e2233efaa037a9154fd7fe2625e4c78d976b/crates/uv-workspace/src/pyproject_mut.rs#L1276 and since we remove them all... there are none. ## Test Plan Manually induced bug with ``` [project] name = "t1" version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.11" dependencies = [ "duct>=0.6.4", "minilog>=2.3.1", # comment ] ``` Then running ``` $ RUST_LOG=trace RUST_BACKTRACE=full uv remove duct minilog DEBUG uv 0.5.8 DEBUG Found project root: `/home/bnorick/dev/workspace/t1` DEBUG No workspace root found, using project root thread 'main' panicked at crates/uv-workspace/src/pyproject_mut.rs:1294:73: called `Option::unwrap()` on a `None` value stack backtrace: 0: 0x5638d7bed6ba - 1: 0x5638d783760b - 2: 0x5638d7bae232 - 3: 0x5638d7bf0f07 - 4: 0x5638d7bf215c - 5: 0x5638d7bf1972 - 6: 0x5638d7bf1909 - 7: 0x5638d7bf18f4 - 8: 0x5638d75087d2 - 9: 0x5638d750896b - 10: 0x5638d7508d68 - 11: 0x5638d8dcf1bb - 12: 0x5638d76be271 - 13: 0x5638d75ef1f9 - 14: 0x5638d75fc3cd - 15: 0x5638d772d9de - 16: 0x5638d8476812 - 17: 0x5638d83e1894 - 18: 0x5638d84722d3 - 19: 0x5638d83e1372 - 20: 0x7f851cfc7d90 - 21: 0x7f851cfc7e40 - __libc_start_main 22: 0x5638d758e992 - 23: 0x0 - ``` --- crates/uv-workspace/src/pyproject_mut.rs | 12 ++--- crates/uv/tests/it/edit.rs | 61 ++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/crates/uv-workspace/src/pyproject_mut.rs b/crates/uv-workspace/src/pyproject_mut.rs index 248bcc103..10d7591ed 100644 --- a/crates/uv-workspace/src/pyproject_mut.rs +++ b/crates/uv-workspace/src/pyproject_mut.rs @@ -1272,15 +1272,11 @@ fn reformat_array_multiline(deps: &mut Array) { .map(|(s, _)| s) .unwrap_or(decor_prefix); - // If there is no indentation, use four-space. - indentation_prefix = Some(if decor_prefix.is_empty() { - " ".to_string() - } else { - decor_prefix.to_string() - }); + indentation_prefix = (!decor_prefix.is_empty()).then_some(decor_prefix.to_string()); } - let indentation_prefix_str = format!("\n{}", indentation_prefix.as_ref().unwrap()); + let indentation_prefix_str = + format!("\n{}", indentation_prefix.as_deref().unwrap_or(" ")); for comment in find_comments(decor.prefix()).chain(find_comments(decor.suffix())) { match comment.comment_type { @@ -1306,7 +1302,7 @@ fn reformat_array_multiline(deps: &mut Array) { match comment.comment_type { CommentType::OwnLine => { let indentation_prefix_str = - format!("\n{}", indentation_prefix.as_ref().unwrap()); + format!("\n{}", indentation_prefix.as_deref().unwrap_or(" ")); rv.push_str(&indentation_prefix_str); } CommentType::EndOfLine => { diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 537f2f83d..84f8cd2ce 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -9024,3 +9024,64 @@ fn remove_requirement() -> Result<()> { Ok(()) } + +/// Remove all dependencies with remaining comments +#[test] +fn remove_all_with_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 = [ + "duct", + "minilog", + # foo + # bar + ] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#})?; + + uv_snapshot!(context.filters(), context.remove().arg("duct").arg("minilog"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + project==0.1.0 (from file://[TEMP_DIR]/) + "###); + + 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 = [ + # foo + # bar + ] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "### + ); + }); + + Ok(()) +}