diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 89f3bd0c198b..1727765a2205 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -338,6 +338,27 @@ pub(crate) async fn add( requirements }; + // If any of the requirements are self-dependencies, bail. + if matches!( + dependency_type, + DependencyType::Production | DependencyType::Dev + ) { + if let Target::Project(project, _) = &target { + if let Some(project_name) = project.project_name() { + for requirement in &requirements { + if requirement.name == *project_name { + bail!( + "Requirement name `{}` matches project name `{}`, but self-dependencies are not permitted. If your project name (`{}`) is shadowing that of a third-party dependency, consider renaming the project.", + requirement.name.cyan(), + project_name.cyan(), + project_name.cyan(), + ); + } + } + } + } + } + // Add the requirements to the `pyproject.toml` or script. let mut toml = match &target { Target::Script(script, _) => { diff --git a/crates/uv/tests/edit.rs b/crates/uv/tests/edit.rs index 68042e69120c..d33e4449c08a 100644 --- a/crates/uv/tests/edit.rs +++ b/crates/uv/tests/edit.rs @@ -4806,6 +4806,7 @@ fn update_offset() -> Result<()> { "### ); }); + Ok(()) } @@ -4865,3 +4866,92 @@ fn add_shadowed_name() -> Result<()> { Ok(()) } + +/// Accidentally add a dependency on the project itself. +#[test] +fn add_self() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "anyio" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#})?; + + uv_snapshot!(context.filters(), context.add().arg("anyio==3.7.0"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Requirement name `anyio` matches project name `anyio`, but self-dependencies are not permitted. If your project name (`anyio`) is shadowing that of a third-party dependency, consider renaming the project. + "###); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "anyio" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [project.optional-dependencies] + types = ["typing-extensions>=4"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#})?; + + // However, recursive extras are fine. + uv_snapshot!(context.filters(), context.add().arg("anyio[types]").arg("--optional").arg("all"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + anyio==0.1.0 (from file://[TEMP_DIR]/) + + typing-extensions==4.10.0 + "###); + + let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?; + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "anyio" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + + [project.optional-dependencies] + types = ["typing-extensions>=4"] + all = [ + "anyio[types]", + ] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + + [tool.uv.sources] + anyio = { workspace = true } + "### + ); + }); + + Ok(()) +}