Respect `-e` flags in `uv add` (#16882)

## Summary

Closes https://github.com/astral-sh/uv/issues/16872.
This commit is contained in:
Charlie Marsh 2025-11-28 10:03:05 -05:00 committed by GitHub
parent e2bda1173e
commit 5498e4d6f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 236 additions and 3 deletions

View File

@ -1697,9 +1697,25 @@ impl Source {
return Ok(None);
}
}
RequirementSource::Path { install_path, .. }
| RequirementSource::Directory { install_path, .. } => Self::Path {
editable,
RequirementSource::Path { install_path, .. } => Self::Path {
editable: None,
package: None,
path: PortablePathBuf::from(
relative_to(&install_path, root)
.or_else(|_| std::path::absolute(&install_path))
.map_err(SourceError::Absolute)?
.into_boxed_path(),
),
marker: MarkerTree::TRUE,
extra: None,
group: None,
},
RequirementSource::Directory {
install_path,
editable: is_editable,
..
} => Self::Path {
editable: editable.or(is_editable),
package: None,
path: PortablePathBuf::from(
relative_to(&install_path, root)

View File

@ -5379,6 +5379,223 @@ fn add_requirements_file() -> Result<()> {
Ok(())
}
/// Add a path dependency from a requirements file, respecting the lack of a `-e` flag.
#[test]
fn add_requirements_file_non_editable() -> Result<()> {
let context = TestContext::new("3.12");
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 = []
"#})?;
// Create a peer package.
let child = context.temp_dir.child("packages").child("child");
child.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "child"
version = "0.1.0"
requires-python = ">=3.12"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
"#})?;
child
.child("src")
.child("child")
.child("__init__.py")
.touch()?;
// Without `-e`, the package should not be listed as editable.
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str("./packages/child")?;
uv_snapshot!(context.filters(), context.add().arg("-r").arg("requirements.txt").arg("--no-workspace"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ child==0.1.0 (from file://[TEMP_DIR]/packages/child)
");
let pyproject_toml_content = context.read("pyproject.toml");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
pyproject_toml_content, @r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"child",
]
[tool.uv.sources]
child = { path = "packages/child" }
"#
);
});
Ok(())
}
/// Add a path dependency from a requirements file, respecting `-e` for editable.
#[test]
fn add_requirements_file_editable() -> Result<()> {
let context = TestContext::new("3.12");
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 = []
"#})?;
// Create a peer package.
let child = context.temp_dir.child("packages").child("child");
child.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "child"
version = "0.1.0"
requires-python = ">=3.12"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
"#})?;
child
.child("src")
.child("child")
.child("__init__.py")
.touch()?;
// With `-e`, the package should be listed as editable.
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str("-e ./packages/child")?;
uv_snapshot!(context.filters(), context.add().arg("-r").arg("requirements.txt").arg("--no-workspace"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ child==0.1.0 (from file://[TEMP_DIR]/packages/child)
");
let pyproject_toml_content = context.read("pyproject.toml");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
pyproject_toml_content, @r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"child",
]
[tool.uv.sources]
child = { path = "packages/child", editable = true }
"#
);
});
Ok(())
}
/// Add a path dependency from a requirements file, overriding the `-e` flag.
#[test]
fn add_requirements_file_editable_override() -> Result<()> {
let context = TestContext::new("3.12");
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 = []
"#})?;
// Create a peer package.
let child = context.temp_dir.child("packages").child("child");
child.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "child"
version = "0.1.0"
requires-python = ">=3.12"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
"#})?;
child
.child("src")
.child("child")
.child("__init__.py")
.touch()?;
// With `-e`, the package should be listed as editable, but the `--no-editable` flag should
// override it.
let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.write_str("-e ./packages/child")?;
uv_snapshot!(context.filters(), context.add().arg("-r").arg("requirements.txt").arg("--no-workspace").arg("--no-editable"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ child==0.1.0 (from file://[TEMP_DIR]/packages/child)
");
let pyproject_toml_content = context.read("pyproject.toml");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
pyproject_toml_content, @r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"child",
]
[tool.uv.sources]
child = { path = "packages/child", editable = false }
"#
);
});
Ok(())
}
/// Add requirements from a file with a marker flag.
///
/// We test that: