Support conflicting editable settings across groups (#14197)

If a user specifies `-e /path/to/dir` and `/path/to/dir` in a `uv pip
install` command, we want the editable to "win" (rather than erroring
due to conflicting URLs). Unfortunately, this behavior meant that when
you requested a package as editable and non-editable in conflicting
groups, the editable version was _always_ used. This PR modifies the
requisite types to use `Option<bool>` rather than `bool` for the
`editable` field, so we can determine whether a requirement was
explicitly requested as editable, explicitly requested as non-editable,
or not specified (as in the case of `/path/to/dir` in a
`requirements.txt` file). In the latter case, we allow editables to
override the "unspecified" requirement.

If a project includes a path dependency twice, once with `editable =
true` and once without any `editable` annotation, those are now
considered conflicting URLs, and lead to an error, so I've marked this
change as breaking.

Closes https://github.com/astral-sh/uv/issues/14139.
This commit is contained in:
Charlie Marsh 2025-07-10 22:20:01 -04:00 committed by Zanie Blue
parent c3d7d3899c
commit dff9ced40a
23 changed files with 530 additions and 216 deletions

View file

@ -2396,8 +2396,8 @@ impl Package {
name: self.id.name.clone(),
url: verbatim_url(&install_path, &self.id)?,
install_path: install_path.into_boxed_path(),
editable: false,
r#virtual: false,
editable: Some(false),
r#virtual: Some(false),
};
uv_distribution_types::SourceDist::Directory(dir_dist)
}
@ -2407,8 +2407,8 @@ impl Package {
name: self.id.name.clone(),
url: verbatim_url(&install_path, &self.id)?,
install_path: install_path.into_boxed_path(),
editable: true,
r#virtual: false,
editable: Some(true),
r#virtual: Some(false),
};
uv_distribution_types::SourceDist::Directory(dir_dist)
}
@ -2418,8 +2418,8 @@ impl Package {
name: self.id.name.clone(),
url: verbatim_url(&install_path, &self.id)?,
install_path: install_path.into_boxed_path(),
editable: false,
r#virtual: true,
editable: Some(false),
r#virtual: Some(true),
};
uv_distribution_types::SourceDist::Directory(dir_dist)
}
@ -3250,9 +3250,9 @@ impl Source {
let path = relative_to(&directory_dist.install_path, root)
.or_else(|_| std::path::absolute(&directory_dist.install_path))
.map_err(LockErrorKind::DistributionRelativePath)?;
if directory_dist.editable {
if directory_dist.editable.unwrap_or(false) {
Ok(Source::Editable(path.into_boxed_path()))
} else if directory_dist.r#virtual {
} else if directory_dist.r#virtual.unwrap_or(false) {
Ok(Source::Virtual(path.into_boxed_path()))
} else {
Ok(Source::Directory(path.into_boxed_path()))
@ -4800,8 +4800,8 @@ fn normalize_requirement(
marker: requires_python.simplify_markers(requirement.marker),
source: RequirementSource::Directory {
install_path,
editable,
r#virtual,
editable: Some(editable.unwrap_or(false)),
r#virtual: Some(r#virtual.unwrap_or(false)),
url,
},
origin: None,