From 9b6901aeea633ed70114f63694a9d339b2037e79 Mon Sep 17 00:00:00 2001 From: Jorge Hermo Date: Wed, 27 Aug 2025 18:28:28 +0200 Subject: [PATCH 1/5] fix: format when project cannot be discovered --- crates/uv/src/commands/project/format.rs | 18 ++- crates/uv/tests/it/format.rs | 146 +++++++++++++++++++++++ 2 files changed, 159 insertions(+), 5 deletions(-) diff --git a/crates/uv/src/commands/project/format.rs b/crates/uv/src/commands/project/format.rs index 7ec3b196706aa..7033dcf316d6d 100644 --- a/crates/uv/src/commands/project/format.rs +++ b/crates/uv/src/commands/project/format.rs @@ -10,7 +10,7 @@ use uv_client::BaseClientBuilder; use uv_pep440::Version; use uv_preview::{Preview, PreviewFeatures}; use uv_warnings::warn_user; -use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache}; +use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache, WorkspaceError}; use crate::child::run_to_completion; use crate::commands::ExitStatus; @@ -40,8 +40,17 @@ pub(crate) async fn format( let workspace_cache = WorkspaceCache::default(); let project = - VirtualProject::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache) - .await?; + VirtualProject::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache).await; + let working_dir = match project { + // If we found a project, we use the project root + Ok(proj) => proj.root().to_owned(), + // If there is a problem finding a project, we just use the provided directory + // e.g. unmanaged projects + Err(WorkspaceError::MissingPyprojectToml) + | Err(WorkspaceError::MissingProject(_)) + | Err(WorkspaceError::NonWorkspace(_)) => project_dir.to_owned(), + Err(err) => return Err(err.into()), + }; // Parse version if provided let version = version.as_deref().map(Version::from_str).transpose()?; @@ -62,8 +71,7 @@ pub(crate) async fn format( .with_context(|| format!("Failed to install ruff {version}"))?; let mut command = Command::new(&ruff_path); - // Run ruff in the project root - command.current_dir(project.root()); + command.current_dir(working_dir); command.arg("format"); if check { diff --git a/crates/uv/tests/it/format.rs b/crates/uv/tests/it/format.rs index b100bd79230d1..fe8251e75f38c 100644 --- a/crates/uv/tests/it/format.rs +++ b/crates/uv/tests/it/format.rs @@ -43,6 +43,108 @@ fn format_project() -> Result<()> { Ok(()) } +#[test] +fn format_missing_pyproject_toml() -> Result<()> { + let context = TestContext::new_with_versions(&[]); + + // Create an unformatted Python file + let main_py = context.temp_dir.child("main.py"); + main_py.write_str(indoc! {r" + x = 1 + "})?; + + uv_snapshot!(context.filters(), context.format(), @r" + success: true + exit_code: 0 + ----- stdout ----- + 1 file reformatted + + ----- stderr ----- + warning: `uv format` is experimental and may change without warning. Pass `--preview-features format` to disable this warning. + "); + + // Check that the file was formatted + let formatted_content = fs_err::read_to_string(&main_py)?; + assert_snapshot!(formatted_content, @r" + x = 1 + "); + + Ok(()) +} + +#[test] +fn format_missing_project_in_pyproject_toml() -> Result<()> { + let context = TestContext::new_with_versions(&[]); + + // Create an empty pyproject.toml with no [project] section + context.temp_dir.child("pyproject.toml"); + + // Create an unformatted Python file + let main_py = context.temp_dir.child("main.py"); + main_py.write_str(indoc! {r" + x = 1 + "})?; + + uv_snapshot!(context.filters(), context.format(), @r" + success: true + exit_code: 0 + ----- stdout ----- + 1 file reformatted + + ----- stderr ----- + warning: `uv format` is experimental and may change without warning. Pass `--preview-features format` to disable this warning. + "); + + // Check that the file was formatted + let formatted_content = fs_err::read_to_string(&main_py)?; + assert_snapshot!(formatted_content, @r" + x = 1 + "); + + Ok(()) +} + +#[test] +fn format_unmanaged_project() -> Result<()> { + let context = TestContext::new_with_versions(&[]); + + 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 = [] + + [tool.uv] + managed = false + "#})?; + + // Create an unformatted Python file + let main_py = context.temp_dir.child("main.py"); + main_py.write_str(indoc! {r" + x = 1 + "})?; + + uv_snapshot!(context.filters(), context.format(), @r" + success: true + exit_code: 0 + ----- stdout ----- + 1 file reformatted + + ----- stderr ----- + warning: `uv format` is experimental and may change without warning. Pass `--preview-features format` to disable this warning. + "); + + // Check that the file was formatted + let formatted_content = fs_err::read_to_string(&main_py)?; + assert_snapshot!(formatted_content, @r" + x = 1 + "); + + Ok(()) +} + #[test] fn format_from_project_root() -> Result<()> { let context = TestContext::new_with_versions(&[]); @@ -135,6 +237,50 @@ fn format_relative_project() -> Result<()> { Ok(()) } +#[test] +fn format_fails_malformed_pyproject() -> Result<()> { + let context = TestContext::new_with_versions(&[]); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str("malformed pyproject.toml")?; + + // Create an unformatted Python file + let main_py = context.temp_dir.child("main.py"); + main_py.write_str(indoc! {r" + x = 1 + "})?; + + uv_snapshot!(context.filters(), context.format(), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: Failed to parse `pyproject.toml` during settings discovery: + TOML parse error at line 1, column 11 + | + 1 | malformed pyproject.toml + | ^ + key with no value, expected `=` + + warning: `uv format` is experimental and may change without warning. Pass `--preview-features format` to disable this warning. + error: Failed to parse: `pyproject.toml` + Caused by: TOML parse error at line 1, column 11 + | + 1 | malformed pyproject.toml + | ^ + key with no value, expected `=` + "); + + // Check that the file is not formatted + let formatted_content = fs_err::read_to_string(&main_py)?; + assert_snapshot!(formatted_content, @r" + x = 1 + "); + + Ok(()) +} + #[test] fn format_check() -> Result<()> { let context = TestContext::new_with_versions(&[]); From 29d1bd6f1f9b3519437b7d55f7d60292745b1f9c Mon Sep 17 00:00:00 2001 From: Jorge Hermo Date: Wed, 27 Aug 2025 18:40:41 +0200 Subject: [PATCH 2/5] chore: apply clippy lints --- crates/uv/src/commands/project/format.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/uv/src/commands/project/format.rs b/crates/uv/src/commands/project/format.rs index 7033dcf316d6d..74934b4d3c606 100644 --- a/crates/uv/src/commands/project/format.rs +++ b/crates/uv/src/commands/project/format.rs @@ -46,9 +46,11 @@ pub(crate) async fn format( Ok(proj) => proj.root().to_owned(), // If there is a problem finding a project, we just use the provided directory // e.g. unmanaged projects - Err(WorkspaceError::MissingPyprojectToml) - | Err(WorkspaceError::MissingProject(_)) - | Err(WorkspaceError::NonWorkspace(_)) => project_dir.to_owned(), + Err( + WorkspaceError::MissingPyprojectToml + | WorkspaceError::MissingProject(_) + | WorkspaceError::NonWorkspace(_), + ) => project_dir.to_owned(), Err(err) => return Err(err.into()), }; From df90d328d74317e9d739018650c0c074c645d6c0 Mon Sep 17 00:00:00 2001 From: Jorge Hermo Date: Wed, 27 Aug 2025 19:08:46 +0200 Subject: [PATCH 3/5] chore: trigger CI again From d949171c2272493305359826b6a36853a8482750 Mon Sep 17 00:00:00 2001 From: Jorge Hermo Date: Fri, 5 Sep 2025 19:26:36 +0200 Subject: [PATCH 4/5] feat: address PR comments --- crates/uv/src/commands/project/format.rs | 31 ++++++++++++------------ 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/crates/uv/src/commands/project/format.rs b/crates/uv/src/commands/project/format.rs index 74934b4d3c606..92711ec1feee7 100644 --- a/crates/uv/src/commands/project/format.rs +++ b/crates/uv/src/commands/project/format.rs @@ -39,20 +39,21 @@ pub(crate) async fn format( } let workspace_cache = WorkspaceCache::default(); - let project = - VirtualProject::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache).await; - let working_dir = match project { - // If we found a project, we use the project root - Ok(proj) => proj.root().to_owned(), - // If there is a problem finding a project, we just use the provided directory - // e.g. unmanaged projects - Err( - WorkspaceError::MissingPyprojectToml - | WorkspaceError::MissingProject(_) - | WorkspaceError::NonWorkspace(_), - ) => project_dir.to_owned(), - Err(err) => return Err(err.into()), - }; + let target_dir = + match VirtualProject::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache) + .await + { + // If we found a project, we use the project root + Ok(proj) => proj.root().to_owned(), + // If there is a problem finding a project, we just use the provided directory + // e.g. unmanaged projects + Err( + WorkspaceError::MissingPyprojectToml + | WorkspaceError::MissingProject(_) + | WorkspaceError::NonWorkspace(_), + ) => project_dir.to_owned(), + Err(err) => return Err(err.into()), + }; // Parse version if provided let version = version.as_deref().map(Version::from_str).transpose()?; @@ -73,7 +74,7 @@ pub(crate) async fn format( .with_context(|| format!("Failed to install ruff {version}"))?; let mut command = Command::new(&ruff_path); - command.current_dir(working_dir); + command.current_dir(target_dir); command.arg("format"); if check { From 186ad3e9473edbb56751828fa2f4ff8d14b5d6a1 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 5 Sep 2025 12:51:37 -0500 Subject: [PATCH 5/5] Update crates/uv/src/commands/project/format.rs --- crates/uv/src/commands/project/format.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/uv/src/commands/project/format.rs b/crates/uv/src/commands/project/format.rs index 92711ec1feee7..e4714a2565431 100644 --- a/crates/uv/src/commands/project/format.rs +++ b/crates/uv/src/commands/project/format.rs @@ -45,8 +45,8 @@ pub(crate) async fn format( { // If we found a project, we use the project root Ok(proj) => proj.root().to_owned(), - // If there is a problem finding a project, we just use the provided directory - // e.g. unmanaged projects + // If there is a problem finding a project, we just use the provided directory, + // e.g., for unmanaged projects Err( WorkspaceError::MissingPyprojectToml | WorkspaceError::MissingProject(_)