mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-03 05:03:46 +00:00
Avoid installing duplicate dependencies across conflicting groups (#11653)
## Summary We need to compute the set of activated groups prior to evaluating the conflict markers on the groups' dependencies. Closes https://github.com/astral-sh/uv/issues/11648.
This commit is contained in:
parent
3365eb4a1c
commit
b588a8ea2f
2 changed files with 137 additions and 5 deletions
|
|
@ -53,6 +53,43 @@ pub trait Installable<'lock> {
|
|||
|
||||
let root = petgraph.add_node(Node::Root);
|
||||
|
||||
// Determine the set of activated extras and groups, from the root.
|
||||
//
|
||||
// TODO(charlie): This isn't quite right. Below, when we add the dependency groups to the
|
||||
// graph, we rely on the activated extras and dependency groups, to evaluate the conflict
|
||||
// marker. But at that point, we don't know the full set of activated extras; this is only
|
||||
// computed below. We somehow need to add the dependency groups _after_ we've computed all
|
||||
// enabled extras, but the groups themselves could depend on the set of enabled extras.
|
||||
if !self.lock().conflicts().is_empty() {
|
||||
for root_name in self.roots() {
|
||||
let dist = self
|
||||
.lock()
|
||||
.find_by_name(root_name)
|
||||
.map_err(|_| LockErrorKind::MultipleRootPackages {
|
||||
name: root_name.clone(),
|
||||
})?
|
||||
.ok_or_else(|| LockErrorKind::MissingRootPackage {
|
||||
name: root_name.clone(),
|
||||
})?;
|
||||
|
||||
// Track the activated extras.
|
||||
if dev.prod() {
|
||||
for extra in extras.extra_names(dist.optional_dependencies.keys()) {
|
||||
activated_extras.push((&dist.id.name, extra));
|
||||
}
|
||||
}
|
||||
|
||||
// Track the activated groups.
|
||||
for group in dist
|
||||
.dependency_groups
|
||||
.keys()
|
||||
.filter(|group| dev.contains(group))
|
||||
{
|
||||
activated_groups.push((&dist.id.name, group));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the workspace packages to the queue.
|
||||
for root_name in self.roots() {
|
||||
let dist = self
|
||||
|
|
@ -77,12 +114,10 @@ pub trait Installable<'lock> {
|
|||
petgraph.add_edge(root, index, Edge::Prod(MarkerTree::TRUE));
|
||||
|
||||
if dev.prod() {
|
||||
// Push its dependencies on the queue and track
|
||||
// activated extras.
|
||||
// Push its dependencies onto the queue.
|
||||
queue.push_back((dist, None));
|
||||
for extra in extras.extra_names(dist.optional_dependencies.keys()) {
|
||||
queue.push_back((dist, Some(extra)));
|
||||
activated_extras.push((&dist.id.name, extra));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -99,10 +134,13 @@ pub trait Installable<'lock> {
|
|||
})
|
||||
.flatten()
|
||||
{
|
||||
if !dep.complexified_marker.evaluate_no_extras(marker_env) {
|
||||
if !dep.complexified_marker.evaluate(
|
||||
marker_env,
|
||||
activated_extras.iter().copied(),
|
||||
activated_groups.iter().copied(),
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
activated_groups.push((&dist.id.name, group));
|
||||
|
||||
let dep_dist = self.lock().find_by_id(&dep.package_id);
|
||||
|
||||
|
|
|
|||
|
|
@ -7714,3 +7714,97 @@ fn unsupported_git_scheme() -> Result<()> {
|
|||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// See: <https://github.com/astral-sh/uv/issues/11648>
|
||||
#[test]
|
||||
fn multiple_group_conflicts() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
let pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
|
||||
[dependency-groups]
|
||||
foo = [
|
||||
"iniconfig>=2",
|
||||
]
|
||||
bar = [
|
||||
"iniconfig<2",
|
||||
]
|
||||
baz = [
|
||||
"iniconfig",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
conflicts = [
|
||||
[
|
||||
{ group = "foo" },
|
||||
{ group = "bar" },
|
||||
],
|
||||
]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 3 packages in [TIME]
|
||||
Audited in [TIME]
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--group").arg("baz"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 3 packages in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ iniconfig==2.0.0
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo").arg("--group").arg("baz"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 3 packages in [TIME]
|
||||
Audited 1 package in [TIME]
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--group").arg("bar").arg("--group").arg("baz"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 3 packages in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Uninstalled 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
- iniconfig==2.0.0
|
||||
+ iniconfig==1.1.1
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo").arg("--group").arg("bar"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 3 packages in [TIME]
|
||||
error: Groups `foo` and `bar` are incompatible with the declared conflicts: {`project:foo`, `project:bar`}
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue