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");