diff --git a/crates/uv/src/commands/pip/operations.rs b/crates/uv/src/commands/pip/operations.rs index 57bfea254..48368bd7e 100644 --- a/crates/uv/src/commands/pip/operations.rs +++ b/crates/uv/src/commands/pip/operations.rs @@ -60,8 +60,18 @@ pub(crate) async fn read_requirements( // If the user requests `extras` but does not provide a valid source (e.g., a `pyproject.toml`), // return an error. if !extras.is_empty() && !requirements.iter().any(RequirementsSource::allows_extras) { + let hint = if requirements.iter().any(|source| { + matches!( + source, + RequirementsSource::Editable(_) | RequirementsSource::SourceTree(_) + ) + }) { + "Use `[extra]` syntax or `-r ` instead." + } else { + "Use `package[extra]` syntax instead." + }; return Err(anyhow!( - "Requesting extras requires a `pyproject.toml`, `setup.cfg`, or `setup.py` file." + "Requesting extras requires a `pyproject.toml`, `setup.cfg`, or `setup.py` file. {hint}" ) .into()); } diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 16cdf7232..ed73ffce0 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -1031,6 +1031,83 @@ fn allow_incompatibilities() -> Result<()> { Ok(()) } +#[test] +fn install_extras() -> Result<()> { + let context = TestContext::new("3.12"); + + // Request extras for an editable path + uv_snapshot!(context.filters(), context.pip_install() + .arg("--all-extras") + .arg("-e") + .arg(context.workspace_root.join("scripts/packages/poetry_editable")), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Requesting extras requires a `pyproject.toml`, `setup.cfg`, or `setup.py` file. Use `[extra]` syntax or `-r ` instead. + "### + ); + + // Request extras for a source tree + uv_snapshot!(context.filters(), context.pip_install() + .arg("--all-extras") + .arg(context.workspace_root.join("scripts/packages/poetry_editable")), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Requesting extras requires a `pyproject.toml`, `setup.cfg`, or `setup.py` file. Use `package[extra]` syntax instead. + "### + ); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("anyio==3.7.0")?; + + // Request extras for a requirements file + uv_snapshot!(context.filters(), context.pip_install() + .arg("--all-extras") + .arg("-r").arg("requirements.txt"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Requesting extras requires a `pyproject.toml`, `setup.cfg`, or `setup.py` file. Use `package[extra]` syntax instead. + "### + ); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" +[project] +name = "project" +version = "0.1.0" +dependencies = ["anyio==3.7.0"] +"#, + )?; + + uv_snapshot!(context.filters(), context.pip_install() + .arg("--all-extras") + .arg("-r").arg("pyproject.toml"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==3.7.0 + + idna==3.6 + + sniffio==1.3.1 + "### + ); + + Ok(()) +} + #[test] fn install_editable() { let context = TestContext::new("3.12");