mirror of https://github.com/astral-sh/uv
uv/tests: adds a test with mutually exclusive extras across a workspace
This tests comes from here: https://github.com/astral-sh/uv/pull/8976#issuecomment-2473672199 And it was originally thought of by Konsti. This test case is the motivation for making `package` optional in `conflicts` instead of forbidding it entirely.
This commit is contained in:
parent
bb78e00a87
commit
c2c9bd9557
|
|
@ -3356,6 +3356,209 @@ fn lock_conflicting_extra_unconditional() -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This tests how we deal with mutually conflicting extras that span multiple
|
||||||
|
/// packages in a workspace.
|
||||||
|
#[test]
|
||||||
|
fn lock_conflicting_extra_nested_across_workspace() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let root_pyproject_toml = context.temp_dir.child("pyproject.toml");
|
||||||
|
root_pyproject_toml.write_str(
|
||||||
|
r#"
|
||||||
|
[project]
|
||||||
|
name = "dummy"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = "==3.12.*"
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
project1 = [
|
||||||
|
"proxy1[project1]",
|
||||||
|
]
|
||||||
|
project2 = [
|
||||||
|
"proxy1[project2]"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
proxy1 = { path = "./proxy1" }
|
||||||
|
dummysub = { workspace = true }
|
||||||
|
|
||||||
|
[tool.uv.workspace]
|
||||||
|
members = ["dummysub"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
conflicts = [
|
||||||
|
[
|
||||||
|
{ extra = "project1" },
|
||||||
|
{ extra = "project2" },
|
||||||
|
],
|
||||||
|
]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let sub_pyproject_toml = context.temp_dir.child("dummysub").child("pyproject.toml");
|
||||||
|
sub_pyproject_toml.write_str(
|
||||||
|
r#"
|
||||||
|
[project]
|
||||||
|
name = "dummysub"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = "==3.12.*"
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
project1 = [
|
||||||
|
"proxy1[project1]",
|
||||||
|
]
|
||||||
|
project2 = [
|
||||||
|
"proxy1[project2]"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
proxy1 = { path = "../proxy1" }
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
conflicts = [
|
||||||
|
[
|
||||||
|
{ extra = "project1" },
|
||||||
|
{ extra = "project2" },
|
||||||
|
],
|
||||||
|
]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let proxy1_pyproject_toml = context.temp_dir.child("proxy1").child("pyproject.toml");
|
||||||
|
proxy1_pyproject_toml.write_str(
|
||||||
|
r#"
|
||||||
|
[project]
|
||||||
|
name = "proxy1"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = "==3.12.*"
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
project1 = ["anyio==4.1.0"]
|
||||||
|
project2 = ["anyio==4.2.0"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// In the scheme above, we declare that `dummy[project1]` conflicts
|
||||||
|
// with `dummy[project2]`, and that `dummysub[project1]` conflicts
|
||||||
|
// with `dummysub[project2]`. But we don't account for the fact that
|
||||||
|
// `dummy[project1]` conflicts with `dummysub[project2]` and that
|
||||||
|
// `dummy[project2]` conflicts with `dummysub[project1]`. So we end
|
||||||
|
// up with a resolution failure.
|
||||||
|
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
× No solution found when resolving dependencies:
|
||||||
|
╰─▶ Because dummy[project2] depends on proxy1[project2] and only proxy1[project2]==0.1.0 is available, we can conclude that dummy[project2] depends on proxy1[project2]==0.1.0. (1)
|
||||||
|
|
||||||
|
Because proxy1[project1]==0.1.0 depends on anyio==4.1.0 and proxy1[project2]==0.1.0 depends on anyio==4.2.0, we can conclude that proxy1[project1]==0.1.0 and proxy1[project2]==0.1.0 are incompatible.
|
||||||
|
And because we know from (1) that dummy[project2] depends on proxy1[project2]==0.1.0, we can conclude that dummy[project2] and proxy1[project1]==0.1.0 are incompatible.
|
||||||
|
And because only proxy1[project1]==0.1.0 is available and dummysub[project1] depends on proxy1[project1], we can conclude that dummysub[project1] and dummy[project2] are incompatible.
|
||||||
|
And because your workspace requires dummy[project2] and dummysub[project1], we can conclude that your workspace's requirements are unsatisfiable.
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Now let's write out the full set of conflicts, taking
|
||||||
|
// advantage of the optional `package` key.
|
||||||
|
root_pyproject_toml.write_str(
|
||||||
|
r#"
|
||||||
|
[project]
|
||||||
|
name = "dummy"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = "==3.12.*"
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
project1 = [
|
||||||
|
"proxy1[project1]",
|
||||||
|
]
|
||||||
|
project2 = [
|
||||||
|
"proxy1[project2]"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
proxy1 = { path = "./proxy1" }
|
||||||
|
dummysub = { workspace = true }
|
||||||
|
|
||||||
|
[tool.uv.workspace]
|
||||||
|
members = ["dummysub"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
conflicts = [
|
||||||
|
[
|
||||||
|
{ extra = "project1" },
|
||||||
|
{ extra = "project2" },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ package = "dummysub", extra = "project1" },
|
||||||
|
{ package = "dummysub", extra = "project2" },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ extra = "project1" },
|
||||||
|
{ package = "dummysub", extra = "project2" },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ package = "dummysub", extra = "project1" },
|
||||||
|
{ extra = "project2" },
|
||||||
|
],
|
||||||
|
]
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
// And we can remove the conflicts from `dummysub` since
|
||||||
|
// there specified in `dummy`.
|
||||||
|
sub_pyproject_toml.write_str(
|
||||||
|
r#"
|
||||||
|
[project]
|
||||||
|
name = "dummysub"
|
||||||
|
version = "0.1.0"
|
||||||
|
requires-python = "==3.12.*"
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
project1 = [
|
||||||
|
"proxy1[project1]",
|
||||||
|
]
|
||||||
|
project2 = [
|
||||||
|
"proxy1[project2]"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.uv.sources]
|
||||||
|
proxy1 = { path = "../proxy1" }
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
// And now things should work.
|
||||||
|
uv_snapshot!(context.filters(), context.lock(), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 7 packages in [TIME]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Show updated dependencies on `lock --upgrade`.
|
/// Show updated dependencies on `lock --upgrade`.
|
||||||
#[test]
|
#[test]
|
||||||
fn lock_upgrade_log() -> Result<()> {
|
fn lock_upgrade_log() -> Result<()> {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue