diff --git a/crates/uv/src/commands/project/init.rs b/crates/uv/src/commands/project/init.rs index 708a7dc04..88e33a41b 100644 --- a/crates/uv/src/commands/project/init.rs +++ b/crates/uv/src/commands/project/init.rs @@ -121,7 +121,10 @@ pub(crate) async fn init( .and_then(|path| path.to_str()) .context("Missing directory name")?; - PackageName::new(name.to_string())? + // Pre-normalize the package name by removing any leading or trailing + // whitespace, and replacing any internal whitespace with hyphens. + let name = name.trim().replace(' ', "-"); + PackageName::new(name)? } }; diff --git a/crates/uv/tests/it/init.rs b/crates/uv/tests/it/init.rs index d43246e87..969aa8d3a 100644 --- a/crates/uv/tests/it/init.rs +++ b/crates/uv/tests/it/init.rs @@ -1218,7 +1218,7 @@ fn init_workspace_outside() -> Result<()> { fn init_normalized_names() -> Result<()> { let context = TestContext::new("3.12"); - // `foo-bar` module is normalized to `foo_bar`. + // `foo-bar` module is normalized to `foo-bar`. uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg("foo-bar").arg("--lib"), @r###" success: true exit_code: 0 @@ -1252,17 +1252,17 @@ fn init_normalized_names() -> Result<()> { ); }); - // `foo-bar` module is normalized to `foo_bar`. - uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg("foo-bar").arg("--app"), @r###" - success: false - exit_code: 2 + // `bar_baz` module is normalized to `bar-baz`. + uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg("bar_baz").arg("--app"), @r###" + success: true + exit_code: 0 ----- stdout ----- ----- stderr ----- - error: Project is already initialized in `[TEMP_DIR]/foo-bar` (`pyproject.toml` file exists) + Initialized project `bar-baz` at `[TEMP_DIR]/bar_baz` "###); - let child = context.temp_dir.child("foo-bar"); + let child = context.temp_dir.child("bar_baz"); let pyproject = fs_err::read_to_string(child.join("pyproject.toml"))?; insta::with_settings!({ @@ -1271,30 +1271,45 @@ fn init_normalized_names() -> Result<()> { assert_snapshot!( pyproject, @r###" [project] - name = "foo-bar" + name = "bar-baz" version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.12" dependencies = [] - - [build-system] - requires = ["hatchling"] - build-backend = "hatchling.build" "### ); }); - // "bar baz" is not allowed. - uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg("bar baz"), @r###" - success: false - exit_code: 2 + // "baz bop" is normalized to "baz-bop". + uv_snapshot!(context.filters(), context.init().current_dir(&context.temp_dir).arg("baz bop"), @r###" + success: true + exit_code: 0 ----- stdout ----- ----- stderr ----- - error: Not a valid package or extra name: "bar baz". Names must start and end with a letter or digit and may only contain -, _, ., and alphanumeric characters. + Initialized project `baz-bop` at `[TEMP_DIR]/baz bop` "###); + let child = context.temp_dir.child("baz bop"); + let pyproject = fs_err::read_to_string(child.join("pyproject.toml"))?; + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject, @r###" + [project] + name = "baz-bop" + version = "0.1.0" + description = "Add your description here" + readme = "README.md" + requires-python = ">=3.12" + dependencies = [] + "### + ); + }); + Ok(()) }