Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions crates/uv/src/commands/project/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -39,9 +39,21 @@ pub(crate) async fn format(
}

let workspace_cache = WorkspaceCache::default();
let project =
VirtualProject::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache)
.await?;
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., for 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()?;
Expand All @@ -62,8 +74,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(target_dir);
command.arg("format");

if check {
Expand Down
146 changes: 146 additions & 0 deletions crates/uv/tests/it/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(&[]);
Expand Down Expand Up @@ -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
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is weird that this error is reported twice... But I see this same happens in other failing tests, is this intended?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the second parse error from ruff?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nope, from the err propagation at format function in format.rs. The first one is the one I'm not sure where is logged

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would presume the first one is when we load global settings? It seems problematic, but out of scope for fixing here if there are other snapshots with the same problem.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay! I'll debug that later and if I found which is the origi, I'll create an issue for it! (ping me if you create it first)

| ^
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(&[]);
Expand Down
Loading