Skip to content

Commit 7aed835

Browse files
committed
uv/tests: adds a test with mutually exclusive extras across a workspace
This tests comes from here: #8976 (comment) 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.
1 parent 181bfe6 commit 7aed835

File tree

1 file changed

+203
-0
lines changed

1 file changed

+203
-0
lines changed

crates/uv/tests/it/lock.rs

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3370,6 +3370,209 @@ fn lock_conflicting_extra_unconditional() -> Result<()> {
33703370
Ok(())
33713371
}
33723372

3373+
/// This tests how we deal with mutually conflicting extras that span multiple
3374+
/// packages in a workspace.
3375+
#[test]
3376+
fn lock_conflicting_extra_nested_across_workspace() -> Result<()> {
3377+
let context = TestContext::new("3.12");
3378+
3379+
let root_pyproject_toml = context.temp_dir.child("pyproject.toml");
3380+
root_pyproject_toml.write_str(
3381+
r#"
3382+
[project]
3383+
name = "dummy"
3384+
version = "0.1.0"
3385+
requires-python = "==3.12.*"
3386+
3387+
[project.optional-dependencies]
3388+
project1 = [
3389+
"proxy1[project1]",
3390+
]
3391+
project2 = [
3392+
"proxy1[project2]"
3393+
]
3394+
3395+
[tool.uv.sources]
3396+
proxy1 = { path = "./proxy1" }
3397+
dummysub = { workspace = true }
3398+
3399+
[tool.uv.workspace]
3400+
members = ["dummysub"]
3401+
3402+
[build-system]
3403+
requires = ["hatchling"]
3404+
build-backend = "hatchling.build"
3405+
3406+
[tool.uv]
3407+
conflicts = [
3408+
[
3409+
{ extra = "project1" },
3410+
{ extra = "project2" },
3411+
],
3412+
]
3413+
"#,
3414+
)?;
3415+
3416+
let sub_pyproject_toml = context.temp_dir.child("dummysub").child("pyproject.toml");
3417+
sub_pyproject_toml.write_str(
3418+
r#"
3419+
[project]
3420+
name = "dummysub"
3421+
version = "0.1.0"
3422+
requires-python = "==3.12.*"
3423+
3424+
[project.optional-dependencies]
3425+
project1 = [
3426+
"proxy1[project1]",
3427+
]
3428+
project2 = [
3429+
"proxy1[project2]"
3430+
]
3431+
3432+
[tool.uv.sources]
3433+
proxy1 = { path = "../proxy1" }
3434+
3435+
[build-system]
3436+
requires = ["hatchling"]
3437+
build-backend = "hatchling.build"
3438+
3439+
[tool.uv]
3440+
conflicts = [
3441+
[
3442+
{ extra = "project1" },
3443+
{ extra = "project2" },
3444+
],
3445+
]
3446+
"#,
3447+
)?;
3448+
3449+
let proxy1_pyproject_toml = context.temp_dir.child("proxy1").child("pyproject.toml");
3450+
proxy1_pyproject_toml.write_str(
3451+
r#"
3452+
[project]
3453+
name = "proxy1"
3454+
version = "0.1.0"
3455+
requires-python = "==3.12.*"
3456+
dependencies = []
3457+
3458+
[project.optional-dependencies]
3459+
project1 = ["anyio==4.1.0"]
3460+
project2 = ["anyio==4.2.0"]
3461+
3462+
[build-system]
3463+
requires = ["hatchling"]
3464+
build-backend = "hatchling.build"
3465+
"#,
3466+
)?;
3467+
3468+
// In the scheme above, we declare that `dummy[project1]` conflicts
3469+
// with `dummy[project2]`, and that `dummysub[project1]` conflicts
3470+
// with `dummysub[project2]`. But we don't account for the fact that
3471+
// `dummy[project1]` conflicts with `dummysub[project2]` and that
3472+
// `dummy[project2]` conflicts with `dummysub[project1]`. So we end
3473+
// up with a resolution failure.
3474+
uv_snapshot!(context.filters(), context.lock(), @r###"
3475+
success: false
3476+
exit_code: 1
3477+
----- stdout -----
3478+
3479+
----- stderr -----
3480+
× No solution found when resolving dependencies:
3481+
╰─▶ 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)
3482+
3483+
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.
3484+
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.
3485+
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.
3486+
And because your workspace requires dummy[project2] and dummysub[project1], we can conclude that your workspace's requirements are unsatisfiable.
3487+
"###);
3488+
3489+
// Now let's write out the full set of conflicts, taking
3490+
// advantage of the optional `package` key.
3491+
root_pyproject_toml.write_str(
3492+
r#"
3493+
[project]
3494+
name = "dummy"
3495+
version = "0.1.0"
3496+
requires-python = "==3.12.*"
3497+
3498+
[project.optional-dependencies]
3499+
project1 = [
3500+
"proxy1[project1]",
3501+
]
3502+
project2 = [
3503+
"proxy1[project2]"
3504+
]
3505+
3506+
[tool.uv.sources]
3507+
proxy1 = { path = "./proxy1" }
3508+
dummysub = { workspace = true }
3509+
3510+
[tool.uv.workspace]
3511+
members = ["dummysub"]
3512+
3513+
[build-system]
3514+
requires = ["hatchling"]
3515+
build-backend = "hatchling.build"
3516+
3517+
[tool.uv]
3518+
conflicts = [
3519+
[
3520+
{ extra = "project1" },
3521+
{ extra = "project2" },
3522+
],
3523+
[
3524+
{ package = "dummysub", extra = "project1" },
3525+
{ package = "dummysub", extra = "project2" },
3526+
],
3527+
[
3528+
{ extra = "project1" },
3529+
{ package = "dummysub", extra = "project2" },
3530+
],
3531+
[
3532+
{ package = "dummysub", extra = "project1" },
3533+
{ extra = "project2" },
3534+
],
3535+
]
3536+
"#,
3537+
)?;
3538+
// And we can remove the conflicts from `dummysub` since
3539+
// there specified in `dummy`.
3540+
sub_pyproject_toml.write_str(
3541+
r#"
3542+
[project]
3543+
name = "dummysub"
3544+
version = "0.1.0"
3545+
requires-python = "==3.12.*"
3546+
3547+
[project.optional-dependencies]
3548+
project1 = [
3549+
"proxy1[project1]",
3550+
]
3551+
project2 = [
3552+
"proxy1[project2]"
3553+
]
3554+
3555+
[tool.uv.sources]
3556+
proxy1 = { path = "../proxy1" }
3557+
3558+
[build-system]
3559+
requires = ["hatchling"]
3560+
build-backend = "hatchling.build"
3561+
"#,
3562+
)?;
3563+
// And now things should work.
3564+
uv_snapshot!(context.filters(), context.lock(), @r###"
3565+
success: true
3566+
exit_code: 0
3567+
----- stdout -----
3568+
3569+
----- stderr -----
3570+
Resolved 7 packages in [TIME]
3571+
"###);
3572+
3573+
Ok(())
3574+
}
3575+
33733576
/// Show updated dependencies on `lock --upgrade`.
33743577
#[test]
33753578
fn lock_upgrade_log() -> Result<()> {

0 commit comments

Comments
 (0)