Skip to content

Commit db821a4

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 fd73767 commit db821a4

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
@@ -3356,6 +3356,209 @@ fn lock_conflicting_extra_unconditional() -> Result<()> {
33563356
Ok(())
33573357
}
33583358

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

0 commit comments

Comments
 (0)