From f8ec7975c8b16a4ae85ae03673222d9f877df0a4 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 4 Nov 2024 16:17:43 -0500 Subject: [PATCH] Update Packse snapshots (#8795) ## Summary The diff here is challenging because it looks like some tests got reordered. --- crates/uv/tests/it/common/mod.rs | 2 +- crates/uv/tests/it/lock_scenarios.rs | 444 +- crates/uv/tests/it/pip_compile_scenarios.rs | 264 +- crates/uv/tests/it/pip_install_scenarios.rs | 5028 +++++++++---------- scripts/scenarios/generate.py | 1 - scripts/scenarios/requirements.in | 2 +- scripts/scenarios/requirements.txt | 68 +- 7 files changed, 2805 insertions(+), 3004 deletions(-) diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 1d509b8d9..3aa7df9e9 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -32,7 +32,7 @@ use uv_static::EnvVars; // Exclude any packages uploaded after this date. static EXCLUDE_NEWER: &str = "2024-03-25T00:00:00Z"; -pub const PACKSE_VERSION: &str = "0.3.37"; +pub const PACKSE_VERSION: &str = "0.3.39"; /// Using a find links url allows using `--index-url` instead of `--extra-index-url` in tests /// to prevent dependency confusion attacks against our test suite. diff --git a/crates/uv/tests/it/lock_scenarios.rs b/crates/uv/tests/it/lock_scenarios.rs index b33346d8a..782902336 100644 --- a/crates/uv/tests/it/lock_scenarios.rs +++ b/crates/uv/tests/it/lock_scenarios.rs @@ -1,7 +1,7 @@ //! DO NOT EDIT //! //! Generated with `./scripts/sync_scenarios.sh` -//! Scenarios from +//! Scenarios from //! #![cfg(all(feature = "python", feature = "pypi"))] #![allow(clippy::needless_raw_string_hashes)] @@ -69,14 +69,14 @@ fn fork_allows_non_conflicting_non_overlapping_dependencies() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 2 packages in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -84,7 +84,7 @@ fn fork_allows_non_conflicting_non_overlapping_dependencies() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -115,7 +115,7 @@ fn fork_allows_non_conflicting_non_overlapping_dependencies() -> Result<()> { { name = "package-a", marker = "sys_platform == 'darwin'", specifier = "<2" }, { name = "package-a", marker = "sys_platform == 'linux'", specifier = ">=1" }, ] - "# + "### ); }); @@ -186,14 +186,14 @@ fn fork_allows_non_conflicting_repeated_dependencies() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 2 packages in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -201,7 +201,7 @@ fn fork_allows_non_conflicting_repeated_dependencies() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.8" @@ -227,7 +227,7 @@ fn fork_allows_non_conflicting_repeated_dependencies() -> Result<()> { { name = "package-a", specifier = "<2" }, { name = "package-a", specifier = ">=1" }, ] - "# + "### ); }); @@ -285,14 +285,14 @@ fn fork_basic() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 3 packages in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -300,7 +300,7 @@ fn fork_basic() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -347,7 +347,7 @@ fn fork_basic() -> Result<()> { { name = "package-a", marker = "sys_platform == 'darwin'", specifier = "<2" }, { name = "package-a", marker = "sys_platform == 'linux'", specifier = ">=2" }, ] - "# + "### ); }); @@ -419,7 +419,7 @@ fn conflict_in_fork() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: false exit_code: 1 ----- stdout ----- @@ -433,7 +433,7 @@ fn conflict_in_fork() -> Result<()> { package-a{sys_platform == 'darwin'}==1.0.0 package-a{sys_platform == 'darwin'}>2 and your project depends on package-a{sys_platform == 'darwin'}<2, we can conclude that your project's requirements are unsatisfiable. - "# + "### ); Ok(()) @@ -487,7 +487,7 @@ fn fork_conflict_unsatisfiable() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: false exit_code: 1 ----- stdout ----- @@ -495,7 +495,7 @@ fn fork_conflict_unsatisfiable() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because your project depends on package-a>=2 and package-a<2, we can conclude that your project's requirements are unsatisfiable. - "# + "### ); Ok(()) @@ -570,14 +570,14 @@ fn fork_filter_sibling_dependencies() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 7 packages in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -585,7 +585,7 @@ fn fork_filter_sibling_dependencies() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -684,7 +684,7 @@ fn fork_filter_sibling_dependencies() -> Result<()> { { name = "package-b", marker = "sys_platform == 'linux'", specifier = "==1.0.0" }, { name = "package-c", marker = "sys_platform == 'darwin'", specifier = "==1.0.0" }, ] - "# + "### ); }); @@ -748,14 +748,14 @@ fn fork_upgrade() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 3 packages in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -763,7 +763,7 @@ fn fork_upgrade() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.8" @@ -798,7 +798,7 @@ fn fork_upgrade() -> Result<()> { [package.metadata] requires-dist = [{ name = "package-foo" }] - "# + "### ); }); @@ -868,14 +868,14 @@ fn fork_incomplete_markers() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 5 packages in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -883,7 +883,7 @@ fn fork_incomplete_markers() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -953,7 +953,7 @@ fn fork_incomplete_markers() -> Result<()> { { name = "package-a", marker = "python_full_version >= '3.11'", specifier = "==2" }, { name = "package-b" }, ] - "# + "### ); }); @@ -1021,14 +1021,14 @@ fn fork_marker_accrue() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 4 packages in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -1036,7 +1036,7 @@ fn fork_marker_accrue() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.8" @@ -1087,7 +1087,7 @@ fn fork_marker_accrue() -> Result<()> { { name = "package-a", marker = "implementation_name == 'cpython'", specifier = "==1.0.0" }, { name = "package-b", marker = "implementation_name == 'pypy'", specifier = "==1.0.0" }, ] - "# + "### ); }); @@ -1154,7 +1154,7 @@ fn fork_marker_disjoint() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: false exit_code: 1 ----- stdout ----- @@ -1162,7 +1162,7 @@ fn fork_marker_disjoint() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies for split (sys_platform == 'linux'): ╰─▶ Because your project depends on package-a{sys_platform == 'linux'}>=2 and package-a{sys_platform == 'linux'}<2, we can conclude that your project's requirements are unsatisfiable. - "# + "### ); Ok(()) @@ -1224,14 +1224,14 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 6 packages in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -1239,7 +1239,7 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -1330,7 +1330,7 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> { { name = "package-a", marker = "sys_platform == 'darwin'", specifier = "<2" }, { name = "package-a", marker = "sys_platform == 'linux'", specifier = ">=2" }, ] - "# + "### ); }); @@ -1404,14 +1404,14 @@ fn fork_marker_inherit_combined_disallowed() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 5 packages in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -1419,7 +1419,7 @@ fn fork_marker_inherit_combined_disallowed() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -1498,7 +1498,7 @@ fn fork_marker_inherit_combined_disallowed() -> Result<()> { { name = "package-a", marker = "sys_platform == 'darwin'", specifier = "<2" }, { name = "package-a", marker = "sys_platform == 'linux'", specifier = ">=2" }, ] - "# + "### ); }); @@ -1573,14 +1573,14 @@ fn fork_marker_inherit_combined() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 5 packages in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -1588,7 +1588,7 @@ fn fork_marker_inherit_combined() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -1667,7 +1667,7 @@ fn fork_marker_inherit_combined() -> Result<()> { { name = "package-a", marker = "sys_platform == 'darwin'", specifier = "<2" }, { name = "package-a", marker = "sys_platform == 'linux'", specifier = ">=2" }, ] - "# + "### ); }); @@ -1735,14 +1735,14 @@ fn fork_marker_inherit_isolated() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 4 packages in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -1750,7 +1750,7 @@ fn fork_marker_inherit_isolated() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -1809,7 +1809,7 @@ fn fork_marker_inherit_isolated() -> Result<()> { { name = "package-a", marker = "sys_platform == 'darwin'", specifier = "<2" }, { name = "package-a", marker = "sys_platform == 'linux'", specifier = ">=2" }, ] - "# + "### ); }); @@ -1883,14 +1883,14 @@ fn fork_marker_inherit_transitive() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 5 packages in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -1898,7 +1898,7 @@ fn fork_marker_inherit_transitive() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -1969,7 +1969,7 @@ fn fork_marker_inherit_transitive() -> Result<()> { { name = "package-a", marker = "sys_platform == 'darwin'", specifier = "<2" }, { name = "package-a", marker = "sys_platform == 'linux'", specifier = ">=2" }, ] - "# + "### ); }); @@ -2039,14 +2039,14 @@ fn fork_marker_inherit() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 3 packages in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -2054,7 +2054,7 @@ fn fork_marker_inherit() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -2101,7 +2101,7 @@ fn fork_marker_inherit() -> Result<()> { { name = "package-a", marker = "sys_platform == 'darwin'", specifier = "<2" }, { name = "package-a", marker = "sys_platform == 'linux'", specifier = ">=2" }, ] - "# + "### ); }); @@ -2177,14 +2177,14 @@ fn fork_marker_limited_inherit() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 5 packages in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -2192,7 +2192,7 @@ fn fork_marker_limited_inherit() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -2262,7 +2262,7 @@ fn fork_marker_limited_inherit() -> Result<()> { { name = "package-a", marker = "sys_platform == 'linux'", specifier = ">=2" }, { name = "package-b" }, ] - "# + "### ); }); @@ -2332,14 +2332,14 @@ fn fork_marker_selection() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 4 packages in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -2347,7 +2347,7 @@ fn fork_marker_selection() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -2405,7 +2405,7 @@ fn fork_marker_selection() -> Result<()> { { name = "package-b", marker = "sys_platform == 'darwin'", specifier = "<2" }, { name = "package-b", marker = "sys_platform == 'linux'", specifier = ">=2" }, ] - "# + "### ); }); @@ -2487,14 +2487,14 @@ fn fork_marker_track() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 5 packages in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -2502,7 +2502,7 @@ fn fork_marker_track() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -2572,7 +2572,7 @@ fn fork_marker_track() -> Result<()> { { name = "package-b", marker = "sys_platform == 'darwin'", specifier = "<2.8" }, { name = "package-b", marker = "sys_platform == 'linux'", specifier = ">=2.8" }, ] - "# + "### ); }); @@ -2639,14 +2639,14 @@ fn fork_non_fork_marker_transitive() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 4 packages in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -2654,7 +2654,7 @@ fn fork_non_fork_marker_transitive() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.8" @@ -2705,7 +2705,7 @@ fn fork_non_fork_marker_transitive() -> Result<()> { { name = "package-a", specifier = "==1.0.0" }, { name = "package-b", specifier = "==1.0.0" }, ] - "# + "### ); }); @@ -2773,7 +2773,7 @@ fn fork_non_local_fork_marker_direct() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: false exit_code: 1 ----- stdout ----- @@ -2782,7 +2782,7 @@ fn fork_non_local_fork_marker_direct() -> Result<()> { × No solution found when resolving dependencies: ╰─▶ Because package-b{sys_platform == 'darwin'}==1.0.0 depends on package-c>=2.0.0 and package-a{sys_platform == 'linux'}==1.0.0 depends on package-c<2.0.0, we can conclude that package-a{sys_platform == 'linux'}==1.0.0 and package-b{sys_platform == 'darwin'}==1.0.0 are incompatible. And because your project depends on package-a{sys_platform == 'linux'}==1.0.0 and package-b{sys_platform == 'darwin'}==1.0.0, we can conclude that your project's requirements are unsatisfiable. - "# + "### ); Ok(()) @@ -2845,7 +2845,7 @@ fn fork_non_local_fork_marker_transitive() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: false exit_code: 1 ----- stdout ----- @@ -2858,7 +2858,7 @@ fn fork_non_local_fork_marker_transitive() -> Result<()> { package-c{sys_platform == 'linux'}>2.0.0 and package-a==1.0.0 depends on package-c{sys_platform == 'linux'}<2.0.0, we can conclude that package-a==1.0.0 and package-b==1.0.0 are incompatible. And because your project depends on package-a==1.0.0 and package-b==1.0.0, we can conclude that your project's requirements are unsatisfiable. - "# + "### ); Ok(()) @@ -2938,14 +2938,14 @@ fn fork_overlapping_markers_basic() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 2 packages in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -2953,7 +2953,7 @@ fn fork_overlapping_markers_basic() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -2985,7 +2985,7 @@ fn fork_overlapping_markers_basic() -> Result<()> { { name = "package-a", marker = "python_full_version >= '3.10'", specifier = ">=1.1.0" }, { name = "package-a", marker = "python_full_version >= '3.11'", specifier = ">=1.2.0" }, ] - "# + "### ); }); @@ -3105,14 +3105,14 @@ fn preferences_dependent_forking_bistable() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 8 packages in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -3120,7 +3120,7 @@ fn preferences_dependent_forking_bistable() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -3225,7 +3225,7 @@ fn preferences_dependent_forking_bistable() -> Result<()> { [package.metadata] requires-dist = [{ name = "package-cleaver" }] - "# + "### ); }); @@ -3341,14 +3341,14 @@ fn preferences_dependent_forking_conflicting() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 6 packages in [TIME] - "# + "### ); Ok(()) @@ -3483,14 +3483,14 @@ fn preferences_dependent_forking_tristable() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 11 packages in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -3498,7 +3498,7 @@ fn preferences_dependent_forking_tristable() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -3651,7 +3651,7 @@ fn preferences_dependent_forking_tristable() -> Result<()> { { name = "package-cleaver" }, { name = "package-foo" }, ] - "# + "### ); }); @@ -3766,14 +3766,14 @@ fn preferences_dependent_forking() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 5 packages in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -3781,7 +3781,7 @@ fn preferences_dependent_forking() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -3852,7 +3852,7 @@ fn preferences_dependent_forking() -> Result<()> { { name = "package-cleaver" }, { name = "package-foo" }, ] - "# + "### ); }); @@ -3940,14 +3940,14 @@ fn fork_remaining_universe_partitioning() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 5 packages in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -3955,7 +3955,7 @@ fn fork_remaining_universe_partitioning() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.8" resolution-markers = [ @@ -4034,7 +4034,7 @@ fn fork_remaining_universe_partitioning() -> Result<()> { { name = "package-a", marker = "sys_platform == 'illumos'", specifier = "<2" }, { name = "package-a", marker = "sys_platform == 'windows'", specifier = ">=2" }, ] - "# + "### ); }); @@ -4092,14 +4092,14 @@ fn fork_requires_python_full_prerelease() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 1 package in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -4107,7 +4107,7 @@ fn fork_requires_python_full_prerelease() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.10" @@ -4118,7 +4118,7 @@ fn fork_requires_python_full_prerelease() -> Result<()> { [package.metadata] requires-dist = [{ name = "package-a", marker = "python_full_version == '3.9'", specifier = "==1.0.0" }] - "# + "### ); }); @@ -4176,14 +4176,14 @@ fn fork_requires_python_full() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 1 package in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -4191,7 +4191,7 @@ fn fork_requires_python_full() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.10" @@ -4202,7 +4202,7 @@ fn fork_requires_python_full() -> Result<()> { [package.metadata] requires-dist = [{ name = "package-a", marker = "python_full_version == '3.9'", specifier = "==1.0.0" }] - "# + "### ); }); @@ -4224,10 +4224,10 @@ fn fork_requires_python_full() -> Result<()> { /// with a `python_version == '3.10'` marker. /// /// This is a regression test for the universal resolver where it would -/// convert a `Requires-Python: >=3.10.1` specifier into a `python_version -/// >= '3.10.1'` marker expression, which would be considered disjoint -/// with `python_version == '3.10'`. Thus, the dependency `a` below was -/// erroneously excluded. It should be included. +/// convert a `Requires-Python: >=3.10.1` specifier into a +/// `python_version >= '3.10.1'` marker expression, which would be +/// considered disjoint with `python_version == '3.10'`. Thus, the +/// dependency `a` below was erroneously excluded. It should be included. /// /// ```text /// fork-requires-python-patch-overlap @@ -4264,14 +4264,14 @@ fn fork_requires_python_patch_overlap() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 2 packages in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -4279,7 +4279,7 @@ fn fork_requires_python_patch_overlap() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.10.1" @@ -4302,7 +4302,7 @@ fn fork_requires_python_patch_overlap() -> Result<()> { [package.metadata] requires-dist = [{ name = "package-a", marker = "python_full_version == '3.10.*'", specifier = "==1.0.0" }] - "# + "### ); }); @@ -4357,14 +4357,14 @@ fn fork_requires_python() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 1 package in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -4372,7 +4372,7 @@ fn fork_requires_python() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.10" @@ -4383,7 +4383,100 @@ fn fork_requires_python() -> Result<()> { [package.metadata] requires-dist = [{ name = "package-a", marker = "python_full_version == '3.9.*'", specifier = "==1.0.0" }] - "# + "### + ); + }); + + // Assert the idempotence of `uv lock` when resolving from the lockfile (`--locked`). + context + .lock() + .arg("--locked") + .env_remove(EnvVars::UV_EXCLUDE_NEWER) + .arg("--index-url") + .arg(packse_index_url()) + .assert() + .success(); + + Ok(()) +} + +/// Check that we only include wheels that match the required Python version +/// +/// ```text +/// requires-python-wheels +/// ├── environment +/// │ └── python3.12 +/// ├── root +/// │ └── requires a==1.0.0 +/// │ └── satisfied by a-1.0.0 +/// └── a +/// └── a-1.0.0 +/// └── requires python>=3.8 +/// ``` +#[test] +fn requires_python_wheels() -> Result<()> { + let context = TestContext::new("3.12"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"requires-python-wheels-", "package-")); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r###" + [project] + name = "project" + version = "0.1.0" + dependencies = [ + '''requires-python-wheels-a==1.0.0''', + ] + requires-python = ">=3.10" + "###, + )?; + + let mut cmd = context.lock(); + cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); + cmd.arg("--index-url").arg(packse_index_url()); + uv_snapshot!(filters, cmd, @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "### + ); + + let lock = context.read("uv.lock"); + insta::with_settings!({ + filters => filters, + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.10" + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "package-a" }, + ] + + [package.metadata] + requires-dist = [{ name = "package-a", specifier = "==1.0.0" }] + + [[package]] + name = "package-a" + version = "1.0.0" + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/requires_python_wheels_a-1.0.0.tar.gz", hash = "sha256:9a11ff73fdc513c4dab0d3e137f4145a00ef0dfc95154360c8f503eed62a03c9" } + wheels = [ + { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/requires_python_wheels_a-1.0.0-cp310-cp310-any.whl", hash = "sha256:b979494a0d7dc825b84d6c516ac407143915f6d2840d229ee2a36b3d06deb61d" }, + { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/requires_python_wheels_a-1.0.0-cp311-cp311-any.whl", hash = "sha256:b979494a0d7dc825b84d6c516ac407143915f6d2840d229ee2a36b3d06deb61d" }, + ] + "### ); }); @@ -4440,14 +4533,14 @@ fn unreachable_package() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 2 packages in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -4455,7 +4548,7 @@ fn unreachable_package() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.8" @@ -4478,7 +4571,7 @@ fn unreachable_package() -> Result<()> { wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/unreachable_package_a-1.0.0-py3-none-any.whl", hash = "sha256:cc472ded9f3b260e6cda0e633fa407a13607e190422cb455f02beebd32d6751f" }, ] - "# + "### ); }); @@ -4541,14 +4634,14 @@ fn unreachable_wheels() -> Result<()> { let mut cmd = context.lock(); cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" + uv_snapshot!(filters, cmd, @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- Resolved 4 packages in [TIME] - "# + "### ); let lock = context.read("uv.lock"); @@ -4556,7 +4649,7 @@ fn unreachable_wheels() -> Result<()> { filters => filters, }, { assert_snapshot!( - lock, @r#" + lock, @r###" version = 1 requires-python = ">=3.8" @@ -4604,100 +4697,7 @@ fn unreachable_wheels() -> Result<()> { wheels = [ { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/unreachable_wheels_c-1.0.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:4b846c5b1646b04828a2bef6c9d180ff7cfd725866013dcec8933de7fb5f9e8d" }, ] - "# - ); - }); - - // Assert the idempotence of `uv lock` when resolving from the lockfile (`--locked`). - context - .lock() - .arg("--locked") - .env_remove(EnvVars::UV_EXCLUDE_NEWER) - .arg("--index-url") - .arg(packse_index_url()) - .assert() - .success(); - - Ok(()) -} - -/// Check that we only include wheels that match the required Python version -/// -/// ```text -/// requires-python-wheels -/// ├── environment -/// │ └── python3.12 -/// ├── root -/// │ └── requires a==1.0.0 -/// │ └── satisfied by a-1.0.0 -/// └── a -/// └── a-1.0.0 -/// └── requires python>=3.8 -/// ``` -#[test] -fn requires_python_wheels() -> Result<()> { - let context = TestContext::new("3.12"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"requires-python-wheels-", "package-")); - - let pyproject_toml = context.temp_dir.child("pyproject.toml"); - pyproject_toml.write_str( - r###" - [project] - name = "project" - version = "0.1.0" - dependencies = [ - '''requires-python-wheels-a==1.0.0''', - ] - requires-python = ">=3.10" - "###, - )?; - - let mut cmd = context.lock(); - cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); - cmd.arg("--index-url").arg(packse_index_url()); - uv_snapshot!(filters, cmd, @r#" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 2 packages in [TIME] - "# - ); - - let lock = context.read("uv.lock"); - insta::with_settings!({ - filters => filters, - }, { - assert_snapshot!( - lock, @r#" - version = 1 - requires-python = ">=3.10" - - [[package]] - name = "project" - version = "0.1.0" - source = { virtual = "." } - dependencies = [ - { name = "package-a" }, - ] - - [package.metadata] - requires-dist = [{ name = "package-a", specifier = "==1.0.0" }] - - [[package]] - name = "package-a" - version = "1.0.0" - source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } - sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/requires_python_wheels_a-1.0.0.tar.gz", hash = "sha256:9a11ff73fdc513c4dab0d3e137f4145a00ef0dfc95154360c8f503eed62a03c9" } - wheels = [ - { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/requires_python_wheels_a-1.0.0-cp310-cp310-any.whl", hash = "sha256:b979494a0d7dc825b84d6c516ac407143915f6d2840d229ee2a36b3d06deb61d" }, - { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/requires_python_wheels_a-1.0.0-cp311-cp311-any.whl", hash = "sha256:b979494a0d7dc825b84d6c516ac407143915f6d2840d229ee2a36b3d06deb61d" }, - ] - "# + "### ); }); diff --git a/crates/uv/tests/it/pip_compile_scenarios.rs b/crates/uv/tests/it/pip_compile_scenarios.rs index 03edb2cc6..8e5a12401 100644 --- a/crates/uv/tests/it/pip_compile_scenarios.rs +++ b/crates/uv/tests/it/pip_compile_scenarios.rs @@ -1,7 +1,7 @@ //! DO NOT EDIT //! //! Generated with `./scripts/sync_scenarios.sh` -//! Scenarios from +//! Scenarios from //! #![cfg(all(feature = "python", feature = "pypi", unix))] @@ -40,59 +40,7 @@ fn command(context: &TestContext, python_versions: &[&str]) -> Command { command } -/// The user requires a package which requires a Python version greater than the -/// current version, but they use an alternative Python version for package -/// resolution. -/// -/// ```text -/// incompatible-python-compatible-override -/// ├── environment -/// │ └── python3.9 -/// ├── root -/// │ └── requires a==1.0.0 -/// │ └── satisfied by a-1.0.0 -/// └── a -/// └── a-1.0.0 -/// └── requires python>=3.10 (incompatible with environment) -/// ``` -#[test] -fn incompatible_python_compatible_override() -> Result<()> { - let context = TestContext::new("3.9"); - let python_versions = &[]; - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"incompatible-python-compatible-override-", "package-")); - - let requirements_in = context.temp_dir.child("requirements.in"); - requirements_in.write_str("incompatible-python-compatible-override-a==1.0.0")?; - - let output = uv_snapshot!(filters, command(&context, python_versions) - .arg("--python-version=3.11") - , @r##" - success: true - exit_code: 0 - ----- stdout ----- - # This file was autogenerated by uv via the following command: - # uv pip compile requirements.in --cache-dir [CACHE_DIR] --python-version=3.11 - package-a==1.0.0 - # via -r requirements.in - - ----- stderr ----- - warning: The requested Python version 3.11 is not available; 3.9.[X] will be used to build dependencies instead. - Resolved 1 package in [TIME] - "## - ); - - output.assert().success().stdout(predicate::str::contains( - "incompatible-python-compatible-override-a==1.0.0", - )); - - Ok(()) -} - -/// The user requires a package which requires a compatible Python version, but they -/// request an incompatible Python version for package resolution. +/// The user requires a package which requires a compatible Python version, but they request an incompatible Python version for package resolution. /// /// ```text /// compatible-python-incompatible-override @@ -119,7 +67,7 @@ fn compatible_python_incompatible_override() -> Result<()> { let output = uv_snapshot!(filters, command(&context, python_versions) .arg("--python-version=3.9") - , @r#" + , @r###" success: false exit_code: 1 ----- stdout ----- @@ -131,7 +79,7 @@ fn compatible_python_incompatible_override() -> Result<()> { And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. hint: The `--python-version` value (>=3.9.0) includes Python versions that are not supported by your dependencies (e.g., package-a==1.0.0 only supports >=3.10). Consider using a higher `--python-version` value. - "# + "### ); output.assert().failure(); @@ -139,66 +87,7 @@ fn compatible_python_incompatible_override() -> Result<()> { Ok(()) } -/// The user requires a package which requires a incompatible Python version, but -/// they request a compatible Python version for package resolution. There are only -/// source distributions available for the package. -/// -/// ```text -/// incompatible-python-compatible-override-unavailable-no-wheels -/// ├── environment -/// │ └── python3.9 -/// ├── root -/// │ └── requires a==1.0.0 -/// │ └── satisfied by a-1.0.0 -/// └── a -/// └── a-1.0.0 -/// └── requires python>=3.10 (incompatible with environment) -/// ``` -#[test] -fn incompatible_python_compatible_override_unavailable_no_wheels() -> Result<()> { - let context = TestContext::new("3.9"); - let python_versions = &[]; - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push(( - r"incompatible-python-compatible-override-unavailable-no-wheels-", - "package-", - )); - - let requirements_in = context.temp_dir.child("requirements.in"); - requirements_in - .write_str("incompatible-python-compatible-override-unavailable-no-wheels-a==1.0.0")?; - - // Since there are no wheels for the package and it is not compatible with the - // local installation, we cannot build the source distribution to determine its - // dependencies. - let output = uv_snapshot!(filters, command(&context, python_versions) - .arg("--python-version=3.11") - , @r###" - success: true - exit_code: 0 - ----- stdout ----- - # This file was autogenerated by uv via the following command: - # uv pip compile requirements.in --cache-dir [CACHE_DIR] --python-version=3.11 - package-a==1.0.0 - # via -r requirements.in - - ----- stderr ----- - warning: The requested Python version 3.11 is not available; 3.9.[X] will be used to build dependencies instead. - Resolved 1 package in [TIME] - "### - ); - - output.assert().success(); - - Ok(()) -} - -/// The user requires a package which requires a incompatible Python version, but -/// they request a compatible Python version for package resolution. There are only -/// source distributions available for the package. The user has a compatible Python -/// version installed elsewhere on their system. +/// The user requires a package which requires a incompatible Python version, but they request a compatible Python version for package resolution. There are only source distributions available for the package. The user has a compatible Python version installed elsewhere on their system. /// /// ```text /// incompatible-python-compatible-override-available-no-wheels @@ -228,11 +117,10 @@ fn incompatible_python_compatible_override_available_no_wheels() -> Result<()> { requirements_in .write_str("incompatible-python-compatible-override-available-no-wheels-a==1.0.0")?; - // Since there is a compatible Python version available on the system, it should be - // used to build the source distributions. + // Since there is a compatible Python version available on the system, it should be used to build the source distributions. let output = uv_snapshot!(filters, command(&context, python_versions) .arg("--python-version=3.11") - , @r##" + , @r###" success: true exit_code: 0 ----- stdout ----- @@ -243,7 +131,7 @@ fn incompatible_python_compatible_override_available_no_wheels() -> Result<()> { ----- stderr ----- Resolved 1 package in [TIME] - "## + "### ); output.assert().success().stdout(predicate::str::contains( @@ -253,9 +141,7 @@ fn incompatible_python_compatible_override_available_no_wheels() -> Result<()> { Ok(()) } -/// The user requires a package which requires a incompatible Python version, but -/// they request a compatible Python version for package resolution. There is a -/// wheel available for the package, but it does not have a compatible tag. +/// The user requires a package which requires a incompatible Python version, but they request a compatible Python version for package resolution. There is a wheel available for the package, but it does not have a compatible tag. /// /// ```text /// incompatible-python-compatible-override-no-compatible-wheels @@ -284,9 +170,7 @@ fn incompatible_python_compatible_override_no_compatible_wheels() -> Result<()> requirements_in .write_str("incompatible-python-compatible-override-no-compatible-wheels-a==1.0.0")?; - // Since there are no compatible wheels for the package and it is not compatible - // with the local installation, we cannot build the source distribution to - // determine its dependencies. + // Since there are no compatible wheels for the package and it is not compatible with the local installation, we cannot build the source distribution to determine its dependencies. However, the source distribution includes static metadata, which we can use to determine dependencies without building the package. let output = uv_snapshot!(filters, command(&context, python_versions) .arg("--python-version=3.11") , @r###" @@ -309,10 +193,7 @@ fn incompatible_python_compatible_override_no_compatible_wheels() -> Result<()> Ok(()) } -/// The user requires a package which requires a incompatible Python version, but -/// they request a compatible Python version for package resolution. There are only -/// source distributions available for the compatible version of the package, but -/// there is an incompatible version with a wheel available. +/// The user requires a package which requires a incompatible Python version, but they request a compatible Python version for package resolution. There are only source distributions available for the compatible version of the package, but there is an incompatible version with a wheel available. /// /// ```text /// incompatible-python-compatible-override-other-wheel @@ -343,10 +224,7 @@ fn incompatible_python_compatible_override_other_wheel() -> Result<()> { let requirements_in = context.temp_dir.child("requirements.in"); requirements_in.write_str("incompatible-python-compatible-override-other-wheel-a")?; - // Since there are no wheels for the version of the package compatible with the - // target and it is not compatible with the local installation, we cannot build the - // source distribution to determine its dependencies. The other version has wheels - // available, but is not compatible with the target version and cannot be used. + // Since there are no wheels for the version of the package compatible with the target and it is not compatible with the local installation, we cannot build the source distribution to determine its dependencies. However, the source distribution includes static metadata, which we can use to determine dependencies without building the package. let output = uv_snapshot!(filters, command(&context, python_versions) .arg("--python-version=3.11") , @r###" @@ -369,8 +247,108 @@ fn incompatible_python_compatible_override_other_wheel() -> Result<()> { Ok(()) } -/// The user requires a package which requires a Python version with a patch version -/// and the user provides a target version without a patch version. +/// The user requires a package which requires a incompatible Python version, but they request a compatible Python version for package resolution. There are only source distributions available for the package. +/// +/// ```text +/// incompatible-python-compatible-override-unavailable-no-wheels +/// ├── environment +/// │ └── python3.9 +/// ├── root +/// │ └── requires a==1.0.0 +/// │ └── satisfied by a-1.0.0 +/// └── a +/// └── a-1.0.0 +/// └── requires python>=3.10 (incompatible with environment) +/// ``` +#[test] +fn incompatible_python_compatible_override_unavailable_no_wheels() -> Result<()> { + let context = TestContext::new("3.9"); + let python_versions = &[]; + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push(( + r"incompatible-python-compatible-override-unavailable-no-wheels-", + "package-", + )); + + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in + .write_str("incompatible-python-compatible-override-unavailable-no-wheels-a==1.0.0")?; + + // Since there are no wheels for the package and it is not compatible with the local installation, we cannot build the source distribution to determine its dependencies. However, the source distribution includes static metadata, which we can use to determine dependencies without building the package. + let output = uv_snapshot!(filters, command(&context, python_versions) + .arg("--python-version=3.11") + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile requirements.in --cache-dir [CACHE_DIR] --python-version=3.11 + package-a==1.0.0 + # via -r requirements.in + + ----- stderr ----- + warning: The requested Python version 3.11 is not available; 3.9.[X] will be used to build dependencies instead. + Resolved 1 package in [TIME] + "### + ); + + output.assert().success(); + + Ok(()) +} + +/// The user requires a package which requires a Python version greater than the current version, but they use an alternative Python version for package resolution. +/// +/// ```text +/// incompatible-python-compatible-override +/// ├── environment +/// │ └── python3.9 +/// ├── root +/// │ └── requires a==1.0.0 +/// │ └── satisfied by a-1.0.0 +/// └── a +/// └── a-1.0.0 +/// └── requires python>=3.10 (incompatible with environment) +/// ``` +#[test] +fn incompatible_python_compatible_override() -> Result<()> { + let context = TestContext::new("3.9"); + let python_versions = &[]; + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"incompatible-python-compatible-override-", "package-")); + + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("incompatible-python-compatible-override-a==1.0.0")?; + + let output = uv_snapshot!(filters, command(&context, python_versions) + .arg("--python-version=3.11") + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile requirements.in --cache-dir [CACHE_DIR] --python-version=3.11 + package-a==1.0.0 + # via -r requirements.in + + ----- stderr ----- + warning: The requested Python version 3.11 is not available; 3.9.[X] will be used to build dependencies instead. + Resolved 1 package in [TIME] + "### + ); + + output.assert().success().stdout(predicate::str::contains( + "incompatible-python-compatible-override-a==1.0.0", + )); + + Ok(()) +} + +/// The user requires a package which requires a Python version with a patch version and the user provides a target version without a patch version. /// /// ```text /// python-patch-override-no-patch @@ -396,11 +374,10 @@ fn python_patch_override_no_patch() -> Result<()> { let requirements_in = context.temp_dir.child("requirements.in"); requirements_in.write_str("python-patch-override-no-patch-a==1.0.0")?; - // Since the resolver is asked to solve with 3.8, the minimum compatible Python - // requirement is treated as 3.8.0. + // Since the resolver is asked to solve with 3.8, the minimum compatible Python requirement is treated as 3.8.0. let output = uv_snapshot!(filters, command(&context, python_versions) .arg("--python-version=3.8") - , @r#" + , @r###" success: false exit_code: 1 ----- stdout ----- @@ -411,7 +388,7 @@ fn python_patch_override_no_patch() -> Result<()> { And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. hint: The `--python-version` value (>=3.8.0) includes Python versions that are not supported by your dependencies (e.g., package-a==1.0.0 only supports >=3.8.4). Consider using a higher `--python-version` value. - "# + "### ); output.assert().failure(); @@ -419,8 +396,7 @@ fn python_patch_override_no_patch() -> Result<()> { Ok(()) } -/// The user requires a package which requires a Python version with a patch version -/// and the user provides a target version with a compatible patch version. +/// The user requires a package which requires a Python version with a patch version and the user provides a target version with a compatible patch version. /// /// ```text /// python-patch-override-patch-compatible @@ -448,7 +424,7 @@ fn python_patch_override_patch_compatible() -> Result<()> { let output = uv_snapshot!(filters, command(&context, python_versions) .arg("--python-version=3.8.0") - , @r##" + , @r###" success: true exit_code: 0 ----- stdout ----- @@ -460,7 +436,7 @@ fn python_patch_override_patch_compatible() -> Result<()> { ----- stderr ----- warning: The requested Python version 3.8.0 is not available; 3.8.18 will be used to build dependencies instead. Resolved 1 package in [TIME] - "## + "### ); output.assert().success().stdout(predicate::str::contains( diff --git a/crates/uv/tests/it/pip_install_scenarios.rs b/crates/uv/tests/it/pip_install_scenarios.rs index 7116d2b4f..57fc63594 100644 --- a/crates/uv/tests/it/pip_install_scenarios.rs +++ b/crates/uv/tests/it/pip_install_scenarios.rs @@ -1,7 +1,7 @@ //! DO NOT EDIT //! //! Generated with `./scripts/sync_scenarios.sh` -//! Scenarios from +//! Scenarios from //! #![cfg(all(feature = "python", feature = "pypi", unix))] @@ -55,43 +55,6 @@ fn command(context: &TestContext) -> Command { command } -/// The user requires any version of package `a` which does not exist. -/// -/// ```text -/// requires-package-does-not-exist -/// ├── environment -/// │ └── python3.8 -/// └── root -/// └── requires a -/// └── unsatisfied: no versions for package -/// ``` -#[test] -fn requires_package_does_not_exist() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"requires-package-does-not-exist-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("requires-package-does-not-exist-a") - , @r#" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because package-a was not found in the package registry and you require package-a, we can conclude that your requirements are unsatisfiable. - "#); - - assert_not_installed( - &context.venv, - "requires_package_does_not_exist_a", - &context.temp_dir, - ); -} - /// The user requires an exact version of package `a` but only other versions exist /// /// ```text @@ -114,7 +77,7 @@ fn requires_exact_version_does_not_exist() { uv_snapshot!(filters, command(&context) .arg("requires-exact-version-does-not-exist-a==2.0.0") - , @r#" + , @r###" success: false exit_code: 1 ----- stdout ----- @@ -122,7 +85,7 @@ fn requires_exact_version_does_not_exist() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because there is no version of package-a==2.0.0 and you require package-a==2.0.0, we can conclude that your requirements are unsatisfiable. - "#); + "###); assert_not_installed( &context.venv, @@ -131,8 +94,7 @@ fn requires_exact_version_does_not_exist() { ); } -/// The user requires a version of `a` greater than `1.0.0` but only smaller or -/// equal versions exist +/// The user requires a version of `a` greater than `1.0.0` but only smaller or equal versions exist /// /// ```text /// requires-greater-version-does-not-exist @@ -155,7 +117,7 @@ fn requires_greater_version_does_not_exist() { uv_snapshot!(filters, command(&context) .arg("requires-greater-version-does-not-exist-a>1.0.0") - , @r#" + , @r###" success: false exit_code: 1 ----- stdout ----- @@ -163,7 +125,7 @@ fn requires_greater_version_does_not_exist() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because only package-a<=1.0.0 is available and you require package-a>1.0.0, we can conclude that your requirements are unsatisfiable. - "#); + "###); assert_not_installed( &context.venv, @@ -172,8 +134,7 @@ fn requires_greater_version_does_not_exist() { ); } -/// The user requires a version of `a` less than `1.0.0` but only larger versions -/// exist +/// The user requires a version of `a` less than `1.0.0` but only larger versions exist /// /// ```text /// requires-less-version-does-not-exist @@ -197,7 +158,7 @@ fn requires_less_version_does_not_exist() { uv_snapshot!(filters, command(&context) .arg("requires-less-version-does-not-exist-a<2.0.0") - , @r#" + , @r###" success: false exit_code: 1 ----- stdout ----- @@ -205,7 +166,7 @@ fn requires_less_version_does_not_exist() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because only package-a>=2.0.0 is available and you require package-a<2.0.0, we can conclude that your requirements are unsatisfiable. - "#); + "###); assert_not_installed( &context.venv, @@ -214,6 +175,43 @@ fn requires_less_version_does_not_exist() { ); } +/// The user requires any version of package `a` which does not exist. +/// +/// ```text +/// requires-package-does-not-exist +/// ├── environment +/// │ └── python3.8 +/// └── root +/// └── requires a +/// └── unsatisfied: no versions for package +/// ``` +#[test] +fn requires_package_does_not_exist() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"requires-package-does-not-exist-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("requires-package-does-not-exist-a") + , @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because package-a was not found in the package registry and you require package-a, we can conclude that your requirements are unsatisfiable. + "###); + + assert_not_installed( + &context.venv, + "requires_package_does_not_exist_a", + &context.temp_dir, + ); +} + /// The user requires package `a` but `a` requires package `b` which does not exist /// /// ```text @@ -238,7 +236,7 @@ fn transitive_requires_package_does_not_exist() { uv_snapshot!(filters, command(&context) .arg("transitive-requires-package-does-not-exist-a") - , @r#" + , @r###" success: false exit_code: 1 ----- stdout ----- @@ -247,7 +245,7 @@ fn transitive_requires_package_does_not_exist() { × No solution found when resolving dependencies: ╰─▶ Because package-b was not found in the package registry and package-a==1.0.0 depends on package-b, we can conclude that package-a==1.0.0 cannot be used. And because only package-a==1.0.0 is available and you require package-a, we can conclude that your requirements are unsatisfiable. - "#); + "###); assert_not_installed( &context.venv, @@ -256,244 +254,7 @@ fn transitive_requires_package_does_not_exist() { ); } -/// Only one version of the requested package is available, but the user has banned -/// that version. -/// -/// ```text -/// excluded-only-version -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a!=1.0.0 -/// │ └── unsatisfied: no matching version -/// └── a -/// └── a-1.0.0 -/// ``` -#[test] -fn excluded_only_version() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"excluded-only-version-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("excluded-only-version-a!=1.0.0") - , @r#" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only package-a==1.0.0 is available and you require one of: - package-a<1.0.0 - package-a>1.0.0 - we can conclude that your requirements are unsatisfiable. - "#); - - // Only `a==1.0.0` is available but the user excluded it. - assert_not_installed(&context.venv, "excluded_only_version_a", &context.temp_dir); -} - -/// Only one version of the requested package `a` is compatible, but the user has -/// banned that version. -/// -/// ```text -/// excluded-only-compatible-version -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ ├── requires a!=2.0.0 -/// │ │ ├── satisfied by a-1.0.0 -/// │ │ └── satisfied by a-3.0.0 -/// │ └── requires b<3.0.0,>=2.0.0 -/// │ └── satisfied by b-2.0.0 -/// ├── a -/// │ ├── a-1.0.0 -/// │ │ └── requires b==1.0.0 -/// │ │ └── satisfied by b-1.0.0 -/// │ ├── a-2.0.0 -/// │ │ └── requires b==2.0.0 -/// │ │ └── satisfied by b-2.0.0 -/// │ └── a-3.0.0 -/// │ └── requires b==3.0.0 -/// │ └── satisfied by b-3.0.0 -/// └── b -/// ├── b-1.0.0 -/// ├── b-2.0.0 -/// └── b-3.0.0 -/// ``` -#[test] -fn excluded_only_compatible_version() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"excluded-only-compatible-version-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("excluded-only-compatible-version-a!=2.0.0") - .arg("excluded-only-compatible-version-b<3.0.0,>=2.0.0") - , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because package-a==1.0.0 depends on package-b==1.0.0 and only the following versions of package-a are available: - package-a==1.0.0 - package-a==2.0.0 - package-a==3.0.0 - we can conclude that package-a<2.0.0 depends on package-b==1.0.0. - And because package-a==3.0.0 depends on package-b==3.0.0, we can conclude that all of: - package-a<2.0.0 - package-a>2.0.0 - depend on one of: - package-b<=1.0.0 - package-b>=3.0.0 - - And because you require one of: - package-a<2.0.0 - package-a>2.0.0 - and package-b>=2.0.0,<3.0.0, we can conclude that your requirements are unsatisfiable. - "###); - - // Only `a==1.2.0` is available since `a==1.0.0` and `a==3.0.0` require - // incompatible versions of `b`. The user has excluded that version of `a` so - // resolution fails. - assert_not_installed( - &context.venv, - "excluded_only_compatible_version_a", - &context.temp_dir, - ); - assert_not_installed( - &context.venv, - "excluded_only_compatible_version_b", - &context.temp_dir, - ); -} - -/// There is a range of compatible versions for the requested package `a`, but -/// another dependency `c` excludes that range. -/// -/// ```text -/// dependency-excludes-range-of-compatible-versions -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ ├── requires a -/// │ │ ├── satisfied by a-1.0.0 -/// │ │ ├── satisfied by a-2.0.0 -/// │ │ ├── satisfied by a-2.1.0 -/// │ │ ├── satisfied by a-2.2.0 -/// │ │ ├── satisfied by a-2.3.0 -/// │ │ └── satisfied by a-3.0.0 -/// │ ├── requires b<3.0.0,>=2.0.0 -/// │ │ └── satisfied by b-2.0.0 -/// │ └── requires c -/// │ ├── satisfied by c-1.0.0 -/// │ └── satisfied by c-2.0.0 -/// ├── a -/// │ ├── a-1.0.0 -/// │ │ └── requires b==1.0.0 -/// │ │ └── satisfied by b-1.0.0 -/// │ ├── a-2.0.0 -/// │ │ └── requires b==2.0.0 -/// │ │ └── satisfied by b-2.0.0 -/// │ ├── a-2.1.0 -/// │ │ └── requires b==2.0.0 -/// │ │ └── satisfied by b-2.0.0 -/// │ ├── a-2.2.0 -/// │ │ └── requires b==2.0.0 -/// │ │ └── satisfied by b-2.0.0 -/// │ ├── a-2.3.0 -/// │ │ └── requires b==2.0.0 -/// │ │ └── satisfied by b-2.0.0 -/// │ └── a-3.0.0 -/// │ └── requires b==3.0.0 -/// │ └── satisfied by b-3.0.0 -/// ├── b -/// │ ├── b-1.0.0 -/// │ ├── b-2.0.0 -/// │ └── b-3.0.0 -/// └── c -/// ├── c-1.0.0 -/// │ └── requires a<2.0.0 -/// │ └── satisfied by a-1.0.0 -/// └── c-2.0.0 -/// └── requires a>=3.0.0 -/// └── satisfied by a-3.0.0 -/// ``` -#[test] -fn dependency_excludes_range_of_compatible_versions() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push(( - r"dependency-excludes-range-of-compatible-versions-", - "package-", - )); - - uv_snapshot!(filters, command(&context) - .arg("dependency-excludes-range-of-compatible-versions-a") - .arg("dependency-excludes-range-of-compatible-versions-b<3.0.0,>=2.0.0") - .arg("dependency-excludes-range-of-compatible-versions-c") - , @r#" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because package-a==1.0.0 depends on package-b==1.0.0 and only the following versions of package-a are available: - package-a==1.0.0 - package-a>2.0.0,<=3.0.0 - we can conclude that package-a<2.0.0 depends on package-b==1.0.0. (1) - - Because only the following versions of package-c are available: - package-c==1.0.0 - package-c==2.0.0 - and package-c==1.0.0 depends on package-a<2.0.0, we can conclude that package-c<2.0.0 depends on package-a<2.0.0. - And because package-c==2.0.0 depends on package-a>=3.0.0, we can conclude that all versions of package-c depend on one of: - package-a<2.0.0 - package-a>=3.0.0 - - And because we know from (1) that package-a<2.0.0 depends on package-b==1.0.0, we can conclude that package-a!=3.0.0, package-b!=1.0.0, all versions of package-c are incompatible. - And because package-a==3.0.0 depends on package-b==3.0.0, we can conclude that all versions of package-c depend on one of: - package-b<=1.0.0 - package-b>=3.0.0 - - And because you require package-b>=2.0.0,<3.0.0 and package-c, we can conclude that your requirements are unsatisfiable. - "#); - - // Only the `2.x` versions of `a` are available since `a==1.0.0` and `a==3.0.0` - // require incompatible versions of `b`, but all available versions of `c` exclude - // that range of `a` so resolution fails. - assert_not_installed( - &context.venv, - "dependency_excludes_range_of_compatible_versions_a", - &context.temp_dir, - ); - assert_not_installed( - &context.venv, - "dependency_excludes_range_of_compatible_versions_b", - &context.temp_dir, - ); - assert_not_installed( - &context.venv, - "dependency_excludes_range_of_compatible_versions_c", - &context.temp_dir, - ); -} - -/// There is a non-contiguous range of compatible versions for the requested package -/// `a`, but another dependency `c` excludes the range. This is the same as -/// `dependency-excludes-range-of-compatible-versions` but some of the versions of -/// `a` are incompatible for another reason e.g. dependency on non-existent package -/// `d`. +/// There is a non-contiguous range of compatible versions for the requested package `a`, but another dependency `c` excludes the range. This is the same as `dependency-excludes-range-of-compatible-versions` but some of the versions of `a` are incompatible for another reason e.g. dependency on non-existent package `d`. /// /// ```text /// dependency-excludes-non-contiguous-range-of-compatible-versions @@ -566,7 +327,7 @@ fn dependency_excludes_non_contiguous_range_of_compatible_versions() { .arg("dependency-excludes-non-contiguous-range-of-compatible-versions-a") .arg("dependency-excludes-non-contiguous-range-of-compatible-versions-b<3.0.0,>=2.0.0") .arg("dependency-excludes-non-contiguous-range-of-compatible-versions-c") - , @r#" + , @r###" success: false exit_code: 1 ----- stdout ----- @@ -592,11 +353,9 @@ fn dependency_excludes_non_contiguous_range_of_compatible_versions() { package-b>=3.0.0 And because you require package-b>=2.0.0,<3.0.0 and package-c, we can conclude that your requirements are unsatisfiable. - "#); + "###); - // Only the `2.x` versions of `a` are available since `a==1.0.0` and `a==3.0.0` - // require incompatible versions of `b`, but all available versions of `c` exclude - // that range of `a` so resolution fails. + // Only the `2.x` versions of `a` are available since `a==1.0.0` and `a==3.0.0` require incompatible versions of `b`, but all available versions of `c` exclude that range of `a` so resolution fails. assert_not_installed( &context.venv, "dependency_excludes_non_contiguous_range_of_compatible_versions_a", @@ -614,167 +373,230 @@ fn dependency_excludes_non_contiguous_range_of_compatible_versions() { ); } -/// Optional dependencies are requested for the package. +/// There is a range of compatible versions for the requested package `a`, but another dependency `c` excludes that range. /// /// ```text -/// extra-required +/// dependency-excludes-range-of-compatible-versions /// ├── environment /// │ └── python3.8 /// ├── root -/// │ └── requires a[extra] -/// │ ├── satisfied by a-1.0.0 -/// │ └── satisfied by a-1.0.0[extra] +/// │ ├── requires a +/// │ │ ├── satisfied by a-1.0.0 +/// │ │ ├── satisfied by a-2.0.0 +/// │ │ ├── satisfied by a-2.1.0 +/// │ │ ├── satisfied by a-2.2.0 +/// │ │ ├── satisfied by a-2.3.0 +/// │ │ └── satisfied by a-3.0.0 +/// │ ├── requires b<3.0.0,>=2.0.0 +/// │ │ └── satisfied by b-2.0.0 +/// │ └── requires c +/// │ ├── satisfied by c-1.0.0 +/// │ └── satisfied by c-2.0.0 /// ├── a /// │ ├── a-1.0.0 -/// │ └── a-1.0.0[extra] -/// │ └── requires b -/// │ └── satisfied by b-1.0.0 -/// └── b -/// └── b-1.0.0 +/// │ │ └── requires b==1.0.0 +/// │ │ └── satisfied by b-1.0.0 +/// │ ├── a-2.0.0 +/// │ │ └── requires b==2.0.0 +/// │ │ └── satisfied by b-2.0.0 +/// │ ├── a-2.1.0 +/// │ │ └── requires b==2.0.0 +/// │ │ └── satisfied by b-2.0.0 +/// │ ├── a-2.2.0 +/// │ │ └── requires b==2.0.0 +/// │ │ └── satisfied by b-2.0.0 +/// │ ├── a-2.3.0 +/// │ │ └── requires b==2.0.0 +/// │ │ └── satisfied by b-2.0.0 +/// │ └── a-3.0.0 +/// │ └── requires b==3.0.0 +/// │ └── satisfied by b-3.0.0 +/// ├── b +/// │ ├── b-1.0.0 +/// │ ├── b-2.0.0 +/// │ └── b-3.0.0 +/// └── c +/// ├── c-1.0.0 +/// │ └── requires a<2.0.0 +/// │ └── satisfied by a-1.0.0 +/// └── c-2.0.0 +/// └── requires a>=3.0.0 +/// └── satisfied by a-3.0.0 /// ``` #[test] -fn extra_required() { +fn dependency_excludes_range_of_compatible_versions() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"extra-required-", "package-")); + filters.push(( + r"dependency-excludes-range-of-compatible-versions-", + "package-", + )); uv_snapshot!(filters, command(&context) - .arg("extra-required-a[extra]") - , @r#" - success: true - exit_code: 0 + .arg("dependency-excludes-range-of-compatible-versions-a") + .arg("dependency-excludes-range-of-compatible-versions-b<3.0.0,>=2.0.0") + .arg("dependency-excludes-range-of-compatible-versions-c") + , @r###" + success: false + exit_code: 1 ----- stdout ----- ----- stderr ----- - Resolved 2 packages in [TIME] - Prepared 2 packages in [TIME] - Installed 2 packages in [TIME] - + package-a==1.0.0 - + package-b==1.0.0 - "#); + × No solution found when resolving dependencies: + ╰─▶ Because package-a==1.0.0 depends on package-b==1.0.0 and only the following versions of package-a are available: + package-a==1.0.0 + package-a>2.0.0,<=3.0.0 + we can conclude that package-a<2.0.0 depends on package-b==1.0.0. (1) - assert_installed( + Because only the following versions of package-c are available: + package-c==1.0.0 + package-c==2.0.0 + and package-c==1.0.0 depends on package-a<2.0.0, we can conclude that package-c<2.0.0 depends on package-a<2.0.0. + And because package-c==2.0.0 depends on package-a>=3.0.0, we can conclude that all versions of package-c depend on one of: + package-a<2.0.0 + package-a>=3.0.0 + + And because we know from (1) that package-a<2.0.0 depends on package-b==1.0.0, we can conclude that package-a!=3.0.0, package-b!=1.0.0, all versions of package-c are incompatible. + And because package-a==3.0.0 depends on package-b==3.0.0, we can conclude that all versions of package-c depend on one of: + package-b<=1.0.0 + package-b>=3.0.0 + + And because you require package-b>=2.0.0,<3.0.0 and package-c, we can conclude that your requirements are unsatisfiable. + "###); + + // Only the `2.x` versions of `a` are available since `a==1.0.0` and `a==3.0.0` require incompatible versions of `b`, but all available versions of `c` exclude that range of `a` so resolution fails. + assert_not_installed( &context.venv, - "extra_required_a", - "1.0.0", + "dependency_excludes_range_of_compatible_versions_a", &context.temp_dir, ); - assert_installed( + assert_not_installed( &context.venv, - "extra_required_b", - "1.0.0", + "dependency_excludes_range_of_compatible_versions_b", + &context.temp_dir, + ); + assert_not_installed( + &context.venv, + "dependency_excludes_range_of_compatible_versions_c", &context.temp_dir, ); } -/// Optional dependencies are requested for the package, but the extra does not -/// exist. +/// Only one version of the requested package `a` is compatible, but the user has banned that version. /// /// ```text -/// missing-extra +/// excluded-only-compatible-version /// ├── environment /// │ └── python3.8 /// ├── root -/// │ └── requires a[extra] -/// │ └── satisfied by a-1.0.0 +/// │ ├── requires a!=2.0.0 +/// │ │ ├── satisfied by a-1.0.0 +/// │ │ └── satisfied by a-3.0.0 +/// │ └── requires b<3.0.0,>=2.0.0 +/// │ └── satisfied by b-2.0.0 +/// ├── a +/// │ ├── a-1.0.0 +/// │ │ └── requires b==1.0.0 +/// │ │ └── satisfied by b-1.0.0 +/// │ ├── a-2.0.0 +/// │ │ └── requires b==2.0.0 +/// │ │ └── satisfied by b-2.0.0 +/// │ └── a-3.0.0 +/// │ └── requires b==3.0.0 +/// │ └── satisfied by b-3.0.0 +/// └── b +/// ├── b-1.0.0 +/// ├── b-2.0.0 +/// └── b-3.0.0 +/// ``` +#[test] +fn excluded_only_compatible_version() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"excluded-only-compatible-version-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("excluded-only-compatible-version-a!=2.0.0") + .arg("excluded-only-compatible-version-b<3.0.0,>=2.0.0") + , @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because package-a==1.0.0 depends on package-b==1.0.0 and only the following versions of package-a are available: + package-a==1.0.0 + package-a==2.0.0 + package-a==3.0.0 + we can conclude that package-a<2.0.0 depends on package-b==1.0.0. + And because package-a==3.0.0 depends on package-b==3.0.0, we can conclude that all of: + package-a<2.0.0 + package-a>2.0.0 + depend on one of: + package-b<=1.0.0 + package-b>=3.0.0 + + And because you require one of: + package-a<2.0.0 + package-a>2.0.0 + and package-b>=2.0.0,<3.0.0, we can conclude that your requirements are unsatisfiable. + "###); + + // Only `a==1.2.0` is available since `a==1.0.0` and `a==3.0.0` require incompatible versions of `b`. The user has excluded that version of `a` so resolution fails. + assert_not_installed( + &context.venv, + "excluded_only_compatible_version_a", + &context.temp_dir, + ); + assert_not_installed( + &context.venv, + "excluded_only_compatible_version_b", + &context.temp_dir, + ); +} + +/// Only one version of the requested package is available, but the user has banned that version. +/// +/// ```text +/// excluded-only-version +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a!=1.0.0 +/// │ └── unsatisfied: no matching version /// └── a /// └── a-1.0.0 /// ``` #[test] -fn missing_extra() { +fn excluded_only_version() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"missing-extra-", "package-")); + filters.push((r"excluded-only-version-", "package-")); uv_snapshot!(filters, command(&context) - .arg("missing-extra-a[extra]") - , @r#" - success: true - exit_code: 0 + .arg("excluded-only-version-a!=1.0.0") + , @r###" + success: false + exit_code: 1 ----- stdout ----- ----- stderr ----- - Resolved 1 package in [TIME] - Prepared 1 package in [TIME] - Installed 1 package in [TIME] - + package-a==1.0.0 - warning: The package `package-a==1.0.0` does not have an extra named `extra` - "#); + × No solution found when resolving dependencies: + ╰─▶ Because only package-a==1.0.0 is available and you require one of: + package-a<1.0.0 + package-a>1.0.0 + we can conclude that your requirements are unsatisfiable. + "###); - // Missing extras are ignored during resolution. - assert_installed(&context.venv, "missing_extra_a", "1.0.0", &context.temp_dir); -} - -/// Multiple optional dependencies are requested for the package. -/// -/// ```text -/// multiple-extras-required -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a[extra_b,extra_c] -/// │ ├── satisfied by a-1.0.0 -/// │ ├── satisfied by a-1.0.0[extra_b] -/// │ └── satisfied by a-1.0.0[extra_c] -/// ├── a -/// │ ├── a-1.0.0 -/// │ ├── a-1.0.0[extra_b] -/// │ │ └── requires b -/// │ │ └── satisfied by b-1.0.0 -/// │ └── a-1.0.0[extra_c] -/// │ └── requires c -/// │ └── satisfied by c-1.0.0 -/// ├── b -/// │ └── b-1.0.0 -/// └── c -/// └── c-1.0.0 -/// ``` -#[test] -fn multiple_extras_required() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"multiple-extras-required-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("multiple-extras-required-a[extra_b,extra_c]") - , @r#" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 3 packages in [TIME] - Prepared 3 packages in [TIME] - Installed 3 packages in [TIME] - + package-a==1.0.0 - + package-b==1.0.0 - + package-c==1.0.0 - "#); - - assert_installed( - &context.venv, - "multiple_extras_required_a", - "1.0.0", - &context.temp_dir, - ); - assert_installed( - &context.venv, - "multiple_extras_required_b", - "1.0.0", - &context.temp_dir, - ); - assert_installed( - &context.venv, - "multiple_extras_required_c", - "1.0.0", - &context.temp_dir, - ); + // Only `a==1.0.0` is available but the user excluded it. + assert_not_installed(&context.venv, "excluded_only_version_a", &context.temp_dir); } /// Multiple optional dependencies are requested for the package via an 'all' extra. @@ -823,7 +645,7 @@ fn all_extras_required() { uv_snapshot!(filters, command(&context) .arg("all-extras-required-a[all]") - , @r#" + , @r###" success: true exit_code: 0 ----- stdout ----- @@ -835,7 +657,7 @@ fn all_extras_required() { + package-a==1.0.0 + package-b==1.0.0 + package-c==1.0.0 - "#); + "###); assert_installed( &context.venv, @@ -857,57 +679,56 @@ fn all_extras_required() { ); } -/// Multiple optional dependencies are requested for the package, but they have -/// conflicting requirements with each other. +/// Optional dependencies are requested for the package, the extra is only available on an older version. /// /// ```text -/// extra-incompatible-with-extra +/// extra-does-not-exist-backtrack /// ├── environment /// │ └── python3.8 /// ├── root -/// │ └── requires a[extra_b,extra_c] +/// │ └── requires a[extra] +/// │ ├── satisfied by a-2.0.0 +/// │ ├── satisfied by a-3.0.0 /// │ ├── satisfied by a-1.0.0 -/// │ ├── satisfied by a-1.0.0[extra_b] -/// │ └── satisfied by a-1.0.0[extra_c] +/// │ └── satisfied by a-1.0.0[extra] /// ├── a +/// │ ├── a-2.0.0 +/// │ ├── a-3.0.0 /// │ ├── a-1.0.0 -/// │ ├── a-1.0.0[extra_b] -/// │ │ └── requires b==1.0.0 -/// │ │ └── satisfied by b-1.0.0 -/// │ └── a-1.0.0[extra_c] -/// │ └── requires b==2.0.0 -/// │ └── satisfied by b-2.0.0 +/// │ └── a-1.0.0[extra] +/// │ └── requires b==1.0.0 +/// │ └── satisfied by b-1.0.0 /// └── b -/// ├── b-1.0.0 -/// └── b-2.0.0 +/// └── b-1.0.0 /// ``` #[test] -fn extra_incompatible_with_extra() { +fn extra_does_not_exist_backtrack() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"extra-incompatible-with-extra-", "package-")); + filters.push((r"extra-does-not-exist-backtrack-", "package-")); uv_snapshot!(filters, command(&context) - .arg("extra-incompatible-with-extra-a[extra_b,extra_c]") - , @r#" - success: false - exit_code: 1 + .arg("extra-does-not-exist-backtrack-a[extra]") + , @r###" + success: true + exit_code: 0 ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only package-a[extra-c]==1.0.0 is available and package-a[extra-c]==1.0.0 depends on package-b==2.0.0, we can conclude that all versions of package-a[extra-c] depend on package-b==2.0.0. - And because package-a[extra-b]==1.0.0 depends on package-b==1.0.0 and only package-a[extra-b]==1.0.0 is available, we can conclude that all versions of package-a[extra-b] and all versions of package-a[extra-c] are incompatible. - And because you require package-a[extra-b] and package-a[extra-c], we can conclude that your requirements are unsatisfiable. - "#); + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + package-a==3.0.0 + warning: The package `package-a==3.0.0` does not have an extra named `extra` + "###); - // Because both `extra_b` and `extra_c` are requested and they require incompatible - // versions of `b`, `a` cannot be installed. - assert_not_installed( + // The resolver should not backtrack to `a==1.0.0` because missing extras are allowed during resolution. `b` should not be installed. + assert_installed( &context.venv, - "extra_incompatible_with_extra_a", + "extra_does_not_exist_backtrack_a", + "3.0.0", &context.temp_dir, ); } @@ -945,7 +766,7 @@ fn extra_incompatible_with_extra_not_requested() { uv_snapshot!(filters, command(&context) .arg("extra-incompatible-with-extra-not-requested-a[extra_c]") - , @r#" + , @r###" success: true exit_code: 0 ----- stdout ----- @@ -956,10 +777,9 @@ fn extra_incompatible_with_extra_not_requested() { Installed 2 packages in [TIME] + package-a==1.0.0 + package-b==2.0.0 - "#); + "###); - // Because the user does not request both extras, it is okay that one is - // incompatible with the other. + // Because the user does not request both extras, it is okay that one is incompatible with the other. assert_installed( &context.venv, "extra_incompatible_with_extra_not_requested_a", @@ -974,8 +794,60 @@ fn extra_incompatible_with_extra_not_requested() { ); } -/// Optional dependencies are requested for the package, but the extra is not -/// compatible with other requested versions. +/// Multiple optional dependencies are requested for the package, but they have conflicting requirements with each other. +/// +/// ```text +/// extra-incompatible-with-extra +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a[extra_b,extra_c] +/// │ ├── satisfied by a-1.0.0 +/// │ ├── satisfied by a-1.0.0[extra_b] +/// │ └── satisfied by a-1.0.0[extra_c] +/// ├── a +/// │ ├── a-1.0.0 +/// │ ├── a-1.0.0[extra_b] +/// │ │ └── requires b==1.0.0 +/// │ │ └── satisfied by b-1.0.0 +/// │ └── a-1.0.0[extra_c] +/// │ └── requires b==2.0.0 +/// │ └── satisfied by b-2.0.0 +/// └── b +/// ├── b-1.0.0 +/// └── b-2.0.0 +/// ``` +#[test] +fn extra_incompatible_with_extra() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"extra-incompatible-with-extra-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("extra-incompatible-with-extra-a[extra_b,extra_c]") + , @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because only package-a[extra-c]==1.0.0 is available and package-a[extra-c]==1.0.0 depends on package-b==2.0.0, we can conclude that all versions of package-a[extra-c] depend on package-b==2.0.0. + And because package-a[extra-b]==1.0.0 depends on package-b==1.0.0 and only package-a[extra-b]==1.0.0 is available, we can conclude that all versions of package-a[extra-b] and all versions of package-a[extra-c] are incompatible. + And because you require package-a[extra-b] and package-a[extra-c], we can conclude that your requirements are unsatisfiable. + "###); + + // Because both `extra_b` and `extra_c` are requested and they require incompatible versions of `b`, `a` cannot be installed. + assert_not_installed( + &context.venv, + "extra_incompatible_with_extra_a", + &context.temp_dir, + ); +} + +/// Optional dependencies are requested for the package, but the extra is not compatible with other requested versions. /// /// ```text /// extra-incompatible-with-root @@ -1007,7 +879,7 @@ fn extra_incompatible_with_root() { uv_snapshot!(filters, command(&context) .arg("extra-incompatible-with-root-a[extra]") .arg("extra-incompatible-with-root-b==2.0.0") - , @r#" + , @r###" success: false exit_code: 1 ----- stdout ----- @@ -1016,10 +888,9 @@ fn extra_incompatible_with_root() { × No solution found when resolving dependencies: ╰─▶ Because only package-a[extra]==1.0.0 is available and package-a[extra]==1.0.0 depends on package-b==1.0.0, we can conclude that all versions of package-a[extra] depend on package-b==1.0.0. And because you require package-a[extra] and package-b==2.0.0, we can conclude that your requirements are unsatisfiable. - "#); + "###); - // Because the user requested `b==2.0.0` but the requested extra requires - // `b==1.0.0`, the dependencies cannot be satisfied. + // Because the user requested `b==2.0.0` but the requested extra requires `b==1.0.0`, the dependencies cannot be satisfied. assert_not_installed( &context.venv, "extra_incompatible_with_root_a", @@ -1032,40 +903,84 @@ fn extra_incompatible_with_root() { ); } -/// Optional dependencies are requested for the package, the extra is only available -/// on an older version. +/// Optional dependencies are requested for the package. /// /// ```text -/// extra-does-not-exist-backtrack +/// extra-required /// ├── environment /// │ └── python3.8 /// ├── root /// │ └── requires a[extra] /// │ ├── satisfied by a-1.0.0 -/// │ ├── satisfied by a-1.0.0[extra] -/// │ ├── satisfied by a-2.0.0 -/// │ └── satisfied by a-3.0.0 +/// │ └── satisfied by a-1.0.0[extra] /// ├── a /// │ ├── a-1.0.0 -/// │ ├── a-1.0.0[extra] -/// │ │ └── requires b==1.0.0 -/// │ │ └── satisfied by b-1.0.0 -/// │ ├── a-2.0.0 -/// │ └── a-3.0.0 +/// │ └── a-1.0.0[extra] +/// │ └── requires b +/// │ └── satisfied by b-1.0.0 /// └── b /// └── b-1.0.0 /// ``` #[test] -fn extra_does_not_exist_backtrack() { +fn extra_required() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"extra-does-not-exist-backtrack-", "package-")); + filters.push((r"extra-required-", "package-")); uv_snapshot!(filters, command(&context) - .arg("extra-does-not-exist-backtrack-a[extra]") - , @r#" + .arg("extra-required-a[extra]") + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + package-a==1.0.0 + + package-b==1.0.0 + "###); + + assert_installed( + &context.venv, + "extra_required_a", + "1.0.0", + &context.temp_dir, + ); + assert_installed( + &context.venv, + "extra_required_b", + "1.0.0", + &context.temp_dir, + ); +} + +/// Optional dependencies are requested for the package, but the extra does not exist. +/// +/// ```text +/// missing-extra +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a[extra] +/// │ └── satisfied by a-1.0.0 +/// └── a +/// └── a-1.0.0 +/// ``` +#[test] +fn missing_extra() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"missing-extra-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("missing-extra-a[extra]") + , @r###" success: true exit_code: 0 ----- stdout ----- @@ -1074,16 +989,78 @@ fn extra_does_not_exist_backtrack() { Resolved 1 package in [TIME] Prepared 1 package in [TIME] Installed 1 package in [TIME] - + package-a==3.0.0 - warning: The package `package-a==3.0.0` does not have an extra named `extra` - "#); + + package-a==1.0.0 + warning: The package `package-a==1.0.0` does not have an extra named `extra` + "###); + + // Missing extras are ignored during resolution. + assert_installed(&context.venv, "missing_extra_a", "1.0.0", &context.temp_dir); +} + +/// Multiple optional dependencies are requested for the package. +/// +/// ```text +/// multiple-extras-required +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a[extra_b,extra_c] +/// │ ├── satisfied by a-1.0.0 +/// │ ├── satisfied by a-1.0.0[extra_b] +/// │ └── satisfied by a-1.0.0[extra_c] +/// ├── a +/// │ ├── a-1.0.0 +/// │ ├── a-1.0.0[extra_b] +/// │ │ └── requires b +/// │ │ └── satisfied by b-1.0.0 +/// │ └── a-1.0.0[extra_c] +/// │ └── requires c +/// │ └── satisfied by c-1.0.0 +/// ├── b +/// │ └── b-1.0.0 +/// └── c +/// └── c-1.0.0 +/// ``` +#[test] +fn multiple_extras_required() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"multiple-extras-required-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("multiple-extras-required-a[extra_b,extra_c]") + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + package-a==1.0.0 + + package-b==1.0.0 + + package-c==1.0.0 + "###); - // The resolver should not backtrack to `a==1.0.0` because missing extras are - // allowed during resolution. `b` should not be installed. assert_installed( &context.venv, - "extra_does_not_exist_backtrack_a", - "3.0.0", + "multiple_extras_required_a", + "1.0.0", + &context.temp_dir, + ); + assert_installed( + &context.venv, + "multiple_extras_required_b", + "1.0.0", + &context.temp_dir, + ); + assert_installed( + &context.venv, + "multiple_extras_required_c", + "1.0.0", &context.temp_dir, ); } @@ -1114,7 +1091,7 @@ fn direct_incompatible_versions() { uv_snapshot!(filters, command(&context) .arg("direct-incompatible-versions-a==1.0.0") .arg("direct-incompatible-versions-a==2.0.0") - , @r#" + , @r###" success: false exit_code: 1 ----- stdout ----- @@ -1122,7 +1099,7 @@ fn direct_incompatible_versions() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because you require package-a==1.0.0 and package-a==2.0.0, we can conclude that your requirements are unsatisfiable. - "#); + "###); assert_not_installed( &context.venv, @@ -1136,8 +1113,51 @@ fn direct_incompatible_versions() { ); } -/// The user requires packages `a` and `b` but `a` requires a different version of -/// `b` +/// The user requires `a`, which requires two incompatible, existing versions of package `b` +/// +/// ```text +/// transitive-incompatible-versions +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a==1.0.0 +/// │ └── satisfied by a-1.0.0 +/// └── a +/// └── a-1.0.0 +/// ├── requires b==2.0.0 +/// └── unsatisfied: no versions for package +/// └── requires b==1.0.0 +/// └── unsatisfied: no versions for package +/// ``` +#[test] +fn transitive_incompatible_versions() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"transitive-incompatible-versions-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("transitive-incompatible-versions-a==1.0.0") + , @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because package-a==1.0.0 depends on package-b==1.0.0 and package-b==2.0.0, we can conclude that package-a==1.0.0 cannot be used. + And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. + "###); + + assert_not_installed( + &context.venv, + "transitive_incompatible_versions_a", + &context.temp_dir, + ); +} + +/// The user requires packages `a` and `b` but `a` requires a different version of `b` /// /// ```text /// transitive-incompatible-with-root-version @@ -1190,8 +1210,7 @@ fn transitive_incompatible_with_root_version() { ); } -/// The user requires package `a` and `b`; `a` and `b` require different versions of -/// `c` +/// The user requires package `a` and `b`; `a` and `b` require different versions of `c` /// /// ```text /// transitive-incompatible-with-transitive @@ -1225,7 +1244,7 @@ fn transitive_incompatible_with_transitive() { uv_snapshot!(filters, command(&context) .arg("transitive-incompatible-with-transitive-a") .arg("transitive-incompatible-with-transitive-b") - , @r#" + , @r###" success: false exit_code: 1 ----- stdout ----- @@ -1235,7 +1254,7 @@ fn transitive_incompatible_with_transitive() { ╰─▶ Because only package-a==1.0.0 is available and package-a==1.0.0 depends on package-c==1.0.0, we can conclude that all versions of package-a depend on package-c==1.0.0. And because package-b==1.0.0 depends on package-c==2.0.0 and only package-b==1.0.0 is available, we can conclude that all versions of package-a and all versions of package-b are incompatible. And because you require package-a and package-b, we can conclude that your requirements are unsatisfiable. - "#); + "###); assert_not_installed( &context.venv, @@ -1249,114 +1268,29 @@ fn transitive_incompatible_with_transitive() { ); } -/// The user requires `a`, which requires two incompatible, existing versions of -/// package `b` +/// A local version should be included in inclusive ordered comparisons. /// /// ```text -/// transitive-incompatible-versions +/// local-greater-than-or-equal /// ├── environment /// │ └── python3.8 /// ├── root -/// │ └── requires a==1.0.0 -/// │ └── satisfied by a-1.0.0 -/// └── a -/// └── a-1.0.0 -/// ├── requires b==2.0.0 -/// └── unsatisfied: no versions for package -/// └── requires b==1.0.0 -/// └── unsatisfied: no versions for package -/// ``` -#[test] -fn transitive_incompatible_versions() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"transitive-incompatible-versions-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("transitive-incompatible-versions-a==1.0.0") - , @r#" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because package-a==1.0.0 depends on package-b==1.0.0 and package-b==2.0.0, we can conclude that package-a==1.0.0 cannot be used. - And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. - "#); - - assert_not_installed( - &context.venv, - "transitive_incompatible_versions_a", - &context.temp_dir, - ); -} - -/// A simple version constraint should not exclude published versions with local -/// segments. -/// -/// ```text -/// local-simple -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a==1.2.3 +/// │ └── requires a>=1.2.3 /// │ └── satisfied by a-1.2.3+foo /// └── a /// └── a-1.2.3+foo /// ``` #[test] -fn local_simple() { +fn local_greater_than_or_equal() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"local-simple-", "package-")); + filters.push((r"local-greater-than-or-equal-", "package-")); uv_snapshot!(filters, command(&context) - .arg("local-simple-a==1.2.3") - , @r#" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because there is no version of package-a==1.2.3 and you require package-a==1.2.3, we can conclude that your requirements are unsatisfiable. - "#); - - // The version '1.2.3+foo' satisfies the constraint '==1.2.3'. - assert_not_installed(&context.venv, "local_simple_a", &context.temp_dir); -} - -/// If there is a 1.2.3 version with an sdist published and no compatible wheels, -/// then the sdist will be used. -/// -/// ```text -/// local-not-used-with-sdist -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a==1.2.3 -/// │ ├── satisfied by a-1.2.3 -/// │ └── satisfied by a-1.2.3+foo -/// └── a -/// ├── a-1.2.3 -/// └── a-1.2.3+foo -/// ``` -#[test] -fn local_not_used_with_sdist() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"local-not-used-with-sdist-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("local-not-used-with-sdist-a==1.2.3") - , @r#" + .arg("local-greater-than-or-equal-a>=1.2.3") + , @r###" success: true exit_code: 0 ----- stdout ----- @@ -1365,64 +1299,129 @@ fn local_not_used_with_sdist() { Resolved 1 package in [TIME] Prepared 1 package in [TIME] Installed 1 package in [TIME] - + package-a==1.2.3 - "#); + + package-a==1.2.3+foo + "###); - // The version '1.2.3' with an sdist satisfies the constraint '==1.2.3'. + // The version '1.2.3+foo' satisfies the constraint '>=1.2.3'. assert_installed( &context.venv, - "local_not_used_with_sdist_a", - "1.2.3", + "local_greater_than_or_equal_a", + "1.2.3+foo", &context.temp_dir, ); } -/// Even if there is a 1.2.3 version published, if it is unavailable for some reason -/// (no sdist and no compatible wheels in this case), a 1.2.3 version with a local -/// segment should be usable instead. +/// A local version should be excluded in exclusive ordered comparisons. /// /// ```text -/// local-used-without-sdist +/// local-greater-than /// ├── environment /// │ └── python3.8 /// ├── root -/// │ └── requires a==1.2.3 -/// │ ├── satisfied by a-1.2.3 -/// │ └── satisfied by a-1.2.3+foo +/// │ └── requires a>1.2.3 +/// │ └── unsatisfied: no matching version /// └── a -/// ├── a-1.2.3 /// └── a-1.2.3+foo /// ``` #[test] -fn local_used_without_sdist() { +fn local_greater_than() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"local-used-without-sdist-", "package-")); + filters.push((r"local-greater-than-", "package-")); uv_snapshot!(filters, command(&context) - .arg("local-used-without-sdist-a==1.2.3") - , @r#" + .arg("local-greater-than-a>1.2.3") + , @r###" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because package-a==1.2.3 has no wheels with a matching Python ABI tag and you require package-a==1.2.3, we can conclude that your requirements are unsatisfiable. - "#); + ╰─▶ Because only package-a==1.2.3+foo is available and you require package-a>1.2.3, we can conclude that your requirements are unsatisfiable. + "###); - // The version '1.2.3+foo' satisfies the constraint '==1.2.3'. + assert_not_installed(&context.venv, "local_greater_than_a", &context.temp_dir); +} + +/// A local version should be included in inclusive ordered comparisons. +/// +/// ```text +/// local-less-than-or-equal +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a<=1.2.3 +/// │ └── satisfied by a-1.2.3+foo +/// └── a +/// └── a-1.2.3+foo +/// ``` +#[test] +fn local_less_than_or_equal() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"local-less-than-or-equal-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("local-less-than-or-equal-a<=1.2.3") + , @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because only package-a==1.2.3+foo is available and you require package-a<=1.2.3, we can conclude that your requirements are unsatisfiable. + "###); + + // The version '1.2.3+foo' satisfies the constraint '<=1.2.3'. assert_not_installed( &context.venv, - "local_used_without_sdist_a", + "local_less_than_or_equal_a", &context.temp_dir, ); } -/// Tests that we can select an older version with a local segment when newer -/// versions are incompatible. +/// A local version should be excluded in exclusive ordered comparisons. +/// +/// ```text +/// local-less-than +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a<1.2.3 +/// │ └── unsatisfied: no matching version +/// └── a +/// └── a-1.2.3+foo +/// ``` +#[test] +fn local_less_than() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"local-less-than-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("local-less-than-a<1.2.3") + , @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because only package-a==1.2.3+foo is available and you require package-a<1.2.3, we can conclude that your requirements are unsatisfiable. + "###); + + assert_not_installed(&context.venv, "local_less_than_a", &context.temp_dir); +} + +/// Tests that we can select an older version with a local segment when newer versions are incompatible. /// /// ```text /// local-not-latest @@ -1448,7 +1447,7 @@ fn local_not_latest() { uv_snapshot!(filters, command(&context) .arg("local-not-latest-a>=1") - , @r#" + , @r###" success: true exit_code: 0 ----- stdout ----- @@ -1458,7 +1457,7 @@ fn local_not_latest() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.2.1+foo - "#); + "###); assert_installed( &context.venv, @@ -1468,94 +1467,73 @@ fn local_not_latest() { ); } -/// A simple version constraint should not exclude published versions with local -/// segments. +/// If there is a 1.2.3 version with an sdist published and no compatible wheels, then the sdist will be used. /// /// ```text -/// local-transitive +/// local-not-used-with-sdist /// ├── environment /// │ └── python3.8 /// ├── root -/// │ ├── requires a -/// │ │ └── satisfied by a-1.0.0 -/// │ └── requires b==2.0.0+foo -/// │ └── satisfied by b-2.0.0+foo -/// ├── a -/// │ └── a-1.0.0 -/// │ └── requires b==2.0.0 -/// │ └── satisfied by b-2.0.0+foo -/// └── b -/// └── b-2.0.0+foo +/// │ └── requires a==1.2.3 +/// │ ├── satisfied by a-1.2.3 +/// │ └── satisfied by a-1.2.3+foo +/// └── a +/// ├── a-1.2.3 +/// └── a-1.2.3+foo /// ``` #[test] -fn local_transitive() { +fn local_not_used_with_sdist() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"local-transitive-", "package-")); + filters.push((r"local-not-used-with-sdist-", "package-")); uv_snapshot!(filters, command(&context) - .arg("local-transitive-a") - .arg("local-transitive-b==2.0.0+foo") - , @r#" + .arg("local-not-used-with-sdist-a==1.2.3") + , @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Resolved 2 packages in [TIME] - Prepared 2 packages in [TIME] - Installed 2 packages in [TIME] - + package-a==1.0.0 - + package-b==2.0.0+foo - "#); + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + package-a==1.2.3 + "###); - // The version '2.0.0+foo' satisfies both ==2.0.0 and ==2.0.0+foo. + // The version '1.2.3' with an sdist satisfies the constraint '==1.2.3'. assert_installed( &context.venv, - "local_transitive_a", - "1.0.0", - &context.temp_dir, - ); - assert_installed( - &context.venv, - "local_transitive_b", - "2.0.0+foo", + "local_not_used_with_sdist_a", + "1.2.3", &context.temp_dir, ); } -/// A transitive constraint on a local version should not match an exclusive ordered -/// operator. +/// A simple version constraint should not exclude published versions with local segments. /// /// ```text -/// local-transitive-greater-than +/// local-simple /// ├── environment /// │ └── python3.8 /// ├── root -/// │ ├── requires a -/// │ │ └── satisfied by a-1.0.0 -/// │ └── requires b==2.0.0+foo -/// │ └── satisfied by b-2.0.0+foo -/// ├── a -/// │ └── a-1.0.0 -/// │ └── requires b>2.0.0 -/// │ └── unsatisfied: no matching version -/// └── b -/// └── b-2.0.0+foo +/// │ └── requires a==1.2.3 +/// │ └── satisfied by a-1.2.3+foo +/// └── a +/// └── a-1.2.3+foo /// ``` #[test] -fn local_transitive_greater_than() { +fn local_simple() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"local-transitive-greater-than-", "package-")); + filters.push((r"local-simple-", "package-")); uv_snapshot!(filters, command(&context) - .arg("local-transitive-greater-than-a") - .arg("local-transitive-greater-than-b==2.0.0+foo") + .arg("local-simple-a==1.2.3") , @r###" success: false exit_code: 1 @@ -1563,164 +1541,49 @@ fn local_transitive_greater_than() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because only package-a==1.0.0 is available and package-a==1.0.0 depends on package-b>2.0.0, we can conclude that all versions of package-a depend on package-b>2.0.0. - And because you require package-a and package-b==2.0.0+foo, we can conclude that your requirements are unsatisfiable. + ╰─▶ Because there is no version of package-a==1.2.3 and you require package-a==1.2.3, we can conclude that your requirements are unsatisfiable. "###); - assert_not_installed( - &context.venv, - "local_transitive_greater_than_a", - &context.temp_dir, - ); - assert_not_installed( - &context.venv, - "local_transitive_greater_than_b", - &context.temp_dir, - ); + // The version '1.2.3+foo' satisfies the constraint '==1.2.3'. + assert_not_installed(&context.venv, "local_simple_a", &context.temp_dir); } -/// A transitive constraint on a local version should match an inclusive ordered -/// operator. +/// A dependency depends on a conflicting local version of a direct dependency, but we can backtrack to a compatible version. /// /// ```text -/// local-transitive-greater-than-or-equal +/// local-transitive-backtrack /// ├── environment /// │ └── python3.8 /// ├── root /// │ ├── requires a -/// │ │ └── satisfied by a-1.0.0 +/// │ │ ├── satisfied by a-1.0.0 +/// │ │ └── satisfied by a-2.0.0 /// │ └── requires b==2.0.0+foo /// │ └── satisfied by b-2.0.0+foo /// ├── a -/// │ └── a-1.0.0 -/// │ └── requires b>=2.0.0 -/// │ └── satisfied by b-2.0.0+foo +/// │ ├── a-1.0.0 +/// │ │ └── requires b==2.0.0 +/// │ │ ├── satisfied by b-2.0.0+foo +/// │ │ └── satisfied by b-2.0.0+bar +/// │ └── a-2.0.0 +/// │ └── requires b==2.0.0+bar +/// │ └── satisfied by b-2.0.0+bar /// └── b -/// └── b-2.0.0+foo +/// ├── b-2.0.0+foo +/// └── b-2.0.0+bar /// ``` #[test] -fn local_transitive_greater_than_or_equal() { +fn local_transitive_backtrack() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"local-transitive-greater-than-or-equal-", "package-")); + filters.push((r"local-transitive-backtrack-", "package-")); uv_snapshot!(filters, command(&context) - .arg("local-transitive-greater-than-or-equal-a") - .arg("local-transitive-greater-than-or-equal-b==2.0.0+foo") - , @r#" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 2 packages in [TIME] - Prepared 2 packages in [TIME] - Installed 2 packages in [TIME] - + package-a==1.0.0 - + package-b==2.0.0+foo - "#); - - // The version '2.0.0+foo' satisfies both >=2.0.0 and ==2.0.0+foo. - assert_installed( - &context.venv, - "local_transitive_greater_than_or_equal_a", - "1.0.0", - &context.temp_dir, - ); - assert_installed( - &context.venv, - "local_transitive_greater_than_or_equal_b", - "2.0.0+foo", - &context.temp_dir, - ); -} - -/// A transitive constraint on a local version should not match an exclusive ordered -/// operator. -/// -/// ```text -/// local-transitive-less-than -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ ├── requires a -/// │ │ └── satisfied by a-1.0.0 -/// │ └── requires b==2.0.0+foo -/// │ └── satisfied by b-2.0.0+foo -/// ├── a -/// │ └── a-1.0.0 -/// │ └── requires b<2.0.0 -/// │ └── unsatisfied: no matching version -/// └── b -/// └── b-2.0.0+foo -/// ``` -#[test] -fn local_transitive_less_than() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"local-transitive-less-than-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("local-transitive-less-than-a") - .arg("local-transitive-less-than-b==2.0.0+foo") + .arg("local-transitive-backtrack-a") + .arg("local-transitive-backtrack-b==2.0.0+foo") , @r###" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only package-a==1.0.0 is available and package-a==1.0.0 depends on package-b<2.0.0, we can conclude that all versions of package-a depend on package-b<2.0.0. - And because you require package-a and package-b==2.0.0+foo, we can conclude that your requirements are unsatisfiable. - "###); - - assert_not_installed( - &context.venv, - "local_transitive_less_than_a", - &context.temp_dir, - ); - assert_not_installed( - &context.venv, - "local_transitive_less_than_b", - &context.temp_dir, - ); -} - -/// A transitive constraint on a local version should match an inclusive ordered -/// operator. -/// -/// ```text -/// local-transitive-less-than-or-equal -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ ├── requires a -/// │ │ └── satisfied by a-1.0.0 -/// │ └── requires b==2.0.0+foo -/// │ └── satisfied by b-2.0.0+foo -/// ├── a -/// │ └── a-1.0.0 -/// │ └── requires b<=2.0.0 -/// │ └── satisfied by b-2.0.0+foo -/// └── b -/// └── b-2.0.0+foo -/// ``` -#[test] -fn local_transitive_less_than_or_equal() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"local-transitive-less-than-or-equal-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("local-transitive-less-than-or-equal-a") - .arg("local-transitive-less-than-or-equal-b==2.0.0+foo") - , @r#" success: true exit_code: 0 ----- stdout ----- @@ -1731,71 +1594,23 @@ fn local_transitive_less_than_or_equal() { Installed 2 packages in [TIME] + package-a==1.0.0 + package-b==2.0.0+foo - "#); + "###); - // The version '2.0.0+foo' satisfies both <=2.0.0 and ==2.0.0+foo. + // Backtracking to '1.0.0' gives us compatible local versions of b. assert_installed( &context.venv, - "local_transitive_less_than_or_equal_a", + "local_transitive_backtrack_a", "1.0.0", &context.temp_dir, ); assert_installed( &context.venv, - "local_transitive_less_than_or_equal_b", + "local_transitive_backtrack_b", "2.0.0+foo", &context.temp_dir, ); } -/// A transitive dependency has both a non-local and local version published, but -/// the non-local version is unusable. -/// -/// ```text -/// local-transitive-confounding -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a -/// │ └── satisfied by a-1.0.0 -/// ├── a -/// │ └── a-1.0.0 -/// │ └── requires b==2.0.0 -/// │ ├── satisfied by b-2.0.0 -/// │ └── satisfied by b-2.0.0+foo -/// └── b -/// ├── b-2.0.0 -/// └── b-2.0.0+foo -/// ``` -#[test] -fn local_transitive_confounding() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"local-transitive-confounding-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("local-transitive-confounding-a") - , @r#" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because package-b==2.0.0 has no wheels with a matching Python ABI tag and package-a==1.0.0 depends on package-b==2.0.0, we can conclude that package-a==1.0.0 cannot be used. - And because only package-a==1.0.0 is available and you require package-a, we can conclude that your requirements are unsatisfiable. - "#); - - // The version '2.0.0+foo' satisfies the constraint '==2.0.0'. - assert_not_installed( - &context.venv, - "local_transitive_confounding_a", - &context.temp_dir, - ); -} - /// A dependency depends on a conflicting local version of a direct dependency. /// /// ```text @@ -1849,43 +1664,83 @@ fn local_transitive_conflicting() { ); } -/// A dependency depends on a conflicting local version of a direct dependency, but -/// we can backtrack to a compatible version. +/// A transitive dependency has both a non-local and local version published, but the non-local version is unusable. /// /// ```text -/// local-transitive-backtrack +/// local-transitive-confounding /// ├── environment /// │ └── python3.8 /// ├── root -/// │ ├── requires a -/// │ │ ├── satisfied by a-1.0.0 -/// │ │ └── satisfied by a-2.0.0 -/// │ └── requires b==2.0.0+foo -/// │ └── satisfied by b-2.0.0+foo +/// │ └── requires a +/// │ └── satisfied by a-1.0.0 /// ├── a -/// │ ├── a-1.0.0 -/// │ │ └── requires b==2.0.0 -/// │ │ ├── satisfied by b-2.0.0+foo -/// │ │ └── satisfied by b-2.0.0+bar -/// │ └── a-2.0.0 -/// │ └── requires b==2.0.0+bar -/// │ └── satisfied by b-2.0.0+bar +/// │ └── a-1.0.0 +/// │ └── requires b==2.0.0 +/// │ ├── satisfied by b-2.0.0 +/// │ └── satisfied by b-2.0.0+foo /// └── b -/// ├── b-2.0.0+foo -/// └── b-2.0.0+bar +/// ├── b-2.0.0 +/// └── b-2.0.0+foo /// ``` #[test] -fn local_transitive_backtrack() { +fn local_transitive_confounding() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"local-transitive-backtrack-", "package-")); + filters.push((r"local-transitive-confounding-", "package-")); uv_snapshot!(filters, command(&context) - .arg("local-transitive-backtrack-a") - .arg("local-transitive-backtrack-b==2.0.0+foo") - , @r#" + .arg("local-transitive-confounding-a") + , @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because package-b==2.0.0 has no wheels with a matching Python ABI tag and package-a==1.0.0 depends on package-b==2.0.0, we can conclude that package-a==1.0.0 cannot be used. + And because only package-a==1.0.0 is available and you require package-a, we can conclude that your requirements are unsatisfiable. + "###); + + // The version '2.0.0+foo' satisfies the constraint '==2.0.0'. + assert_not_installed( + &context.venv, + "local_transitive_confounding_a", + &context.temp_dir, + ); +} + +/// A transitive constraint on a local version should match an inclusive ordered operator. +/// +/// ```text +/// local-transitive-greater-than-or-equal +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ ├── requires a +/// │ │ └── satisfied by a-1.0.0 +/// │ └── requires b==2.0.0+foo +/// │ └── satisfied by b-2.0.0+foo +/// ├── a +/// │ └── a-1.0.0 +/// │ └── requires b>=2.0.0 +/// │ └── satisfied by b-2.0.0+foo +/// └── b +/// └── b-2.0.0+foo +/// ``` +#[test] +fn local_transitive_greater_than_or_equal() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"local-transitive-greater-than-or-equal-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("local-transitive-greater-than-or-equal-a") + .arg("local-transitive-greater-than-or-equal-b==2.0.0+foo") + , @r###" success: true exit_code: 0 ----- stdout ----- @@ -1896,81 +1751,307 @@ fn local_transitive_backtrack() { Installed 2 packages in [TIME] + package-a==1.0.0 + package-b==2.0.0+foo - "#); + "###); - // Backtracking to '1.0.0' gives us compatible local versions of b. + // The version '2.0.0+foo' satisfies both >=2.0.0 and ==2.0.0+foo. assert_installed( &context.venv, - "local_transitive_backtrack_a", + "local_transitive_greater_than_or_equal_a", "1.0.0", &context.temp_dir, ); assert_installed( &context.venv, - "local_transitive_backtrack_b", + "local_transitive_greater_than_or_equal_b", "2.0.0+foo", &context.temp_dir, ); } -/// A local version should be excluded in exclusive ordered comparisons. +/// A transitive constraint on a local version should not match an exclusive ordered operator. /// /// ```text -/// local-greater-than +/// local-transitive-greater-than /// ├── environment /// │ └── python3.8 /// ├── root -/// │ └── requires a>1.2.3 -/// │ └── unsatisfied: no matching version -/// └── a -/// └── a-1.2.3+foo +/// │ ├── requires a +/// │ │ └── satisfied by a-1.0.0 +/// │ └── requires b==2.0.0+foo +/// │ └── satisfied by b-2.0.0+foo +/// ├── a +/// │ └── a-1.0.0 +/// │ └── requires b>2.0.0 +/// │ └── unsatisfied: no matching version +/// └── b +/// └── b-2.0.0+foo /// ``` #[test] -fn local_greater_than() { +fn local_transitive_greater_than() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"local-greater-than-", "package-")); + filters.push((r"local-transitive-greater-than-", "package-")); uv_snapshot!(filters, command(&context) - .arg("local-greater-than-a>1.2.3") - , @r#" + .arg("local-transitive-greater-than-a") + .arg("local-transitive-greater-than-b==2.0.0+foo") + , @r###" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because only package-a==1.2.3+foo is available and you require package-a>1.2.3, we can conclude that your requirements are unsatisfiable. - "#); + ╰─▶ Because only package-a==1.0.0 is available and package-a==1.0.0 depends on package-b>2.0.0, we can conclude that all versions of package-a depend on package-b>2.0.0. + And because you require package-a and package-b==2.0.0+foo, we can conclude that your requirements are unsatisfiable. + "###); - assert_not_installed(&context.venv, "local_greater_than_a", &context.temp_dir); + assert_not_installed( + &context.venv, + "local_transitive_greater_than_a", + &context.temp_dir, + ); + assert_not_installed( + &context.venv, + "local_transitive_greater_than_b", + &context.temp_dir, + ); } -/// A local version should be included in inclusive ordered comparisons. +/// A transitive constraint on a local version should match an inclusive ordered operator. /// /// ```text -/// local-greater-than-or-equal +/// local-transitive-less-than-or-equal /// ├── environment /// │ └── python3.8 /// ├── root -/// │ └── requires a>=1.2.3 -/// │ └── satisfied by a-1.2.3+foo -/// └── a -/// └── a-1.2.3+foo +/// │ ├── requires a +/// │ │ └── satisfied by a-1.0.0 +/// │ └── requires b==2.0.0+foo +/// │ └── satisfied by b-2.0.0+foo +/// ├── a +/// │ └── a-1.0.0 +/// │ └── requires b<=2.0.0 +/// │ └── satisfied by b-2.0.0+foo +/// └── b +/// └── b-2.0.0+foo /// ``` #[test] -fn local_greater_than_or_equal() { +fn local_transitive_less_than_or_equal() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"local-greater-than-or-equal-", "package-")); + filters.push((r"local-transitive-less-than-or-equal-", "package-")); uv_snapshot!(filters, command(&context) - .arg("local-greater-than-or-equal-a>=1.2.3") - , @r#" + .arg("local-transitive-less-than-or-equal-a") + .arg("local-transitive-less-than-or-equal-b==2.0.0+foo") + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + package-a==1.0.0 + + package-b==2.0.0+foo + "###); + + // The version '2.0.0+foo' satisfies both <=2.0.0 and ==2.0.0+foo. + assert_installed( + &context.venv, + "local_transitive_less_than_or_equal_a", + "1.0.0", + &context.temp_dir, + ); + assert_installed( + &context.venv, + "local_transitive_less_than_or_equal_b", + "2.0.0+foo", + &context.temp_dir, + ); +} + +/// A transitive constraint on a local version should not match an exclusive ordered operator. +/// +/// ```text +/// local-transitive-less-than +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ ├── requires a +/// │ │ └── satisfied by a-1.0.0 +/// │ └── requires b==2.0.0+foo +/// │ └── satisfied by b-2.0.0+foo +/// ├── a +/// │ └── a-1.0.0 +/// │ └── requires b<2.0.0 +/// │ └── unsatisfied: no matching version +/// └── b +/// └── b-2.0.0+foo +/// ``` +#[test] +fn local_transitive_less_than() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"local-transitive-less-than-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("local-transitive-less-than-a") + .arg("local-transitive-less-than-b==2.0.0+foo") + , @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because only package-a==1.0.0 is available and package-a==1.0.0 depends on package-b<2.0.0, we can conclude that all versions of package-a depend on package-b<2.0.0. + And because you require package-a and package-b==2.0.0+foo, we can conclude that your requirements are unsatisfiable. + "###); + + assert_not_installed( + &context.venv, + "local_transitive_less_than_a", + &context.temp_dir, + ); + assert_not_installed( + &context.venv, + "local_transitive_less_than_b", + &context.temp_dir, + ); +} + +/// A simple version constraint should not exclude published versions with local segments. +/// +/// ```text +/// local-transitive +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ ├── requires a +/// │ │ └── satisfied by a-1.0.0 +/// │ └── requires b==2.0.0+foo +/// │ └── satisfied by b-2.0.0+foo +/// ├── a +/// │ └── a-1.0.0 +/// │ └── requires b==2.0.0 +/// │ └── satisfied by b-2.0.0+foo +/// └── b +/// └── b-2.0.0+foo +/// ``` +#[test] +fn local_transitive() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"local-transitive-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("local-transitive-a") + .arg("local-transitive-b==2.0.0+foo") + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + package-a==1.0.0 + + package-b==2.0.0+foo + "###); + + // The version '2.0.0+foo' satisfies both ==2.0.0 and ==2.0.0+foo. + assert_installed( + &context.venv, + "local_transitive_a", + "1.0.0", + &context.temp_dir, + ); + assert_installed( + &context.venv, + "local_transitive_b", + "2.0.0+foo", + &context.temp_dir, + ); +} + +/// Even if there is a 1.2.3 version published, if it is unavailable for some reason (no sdist and no compatible wheels in this case), a 1.2.3 version with a local segment should be usable instead. +/// +/// ```text +/// local-used-without-sdist +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a==1.2.3 +/// │ ├── satisfied by a-1.2.3 +/// │ └── satisfied by a-1.2.3+foo +/// └── a +/// ├── a-1.2.3 +/// └── a-1.2.3+foo +/// ``` +#[test] +fn local_used_without_sdist() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"local-used-without-sdist-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("local-used-without-sdist-a==1.2.3") + , @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because package-a==1.2.3 has no wheels with a matching Python ABI tag and you require package-a==1.2.3, we can conclude that your requirements are unsatisfiable. + "###); + + // The version '1.2.3+foo' satisfies the constraint '==1.2.3'. + assert_not_installed( + &context.venv, + "local_used_without_sdist_a", + &context.temp_dir, + ); +} + +/// An equal version constraint should match a post-release version if the post-release version is available. +/// +/// ```text +/// post-equal-available +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a==1.2.3.post0 +/// │ └── satisfied by a-1.2.3.post0 +/// └── a +/// ├── a-1.2.3 +/// └── a-1.2.3.post0 +/// ``` +#[test] +fn post_equal_available() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"post-equal-available-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("post-equal-available-a==1.2.3.post0") + , @r###" success: true exit_code: 0 ----- stdout ----- @@ -1979,126 +2060,101 @@ fn local_greater_than_or_equal() { Resolved 1 package in [TIME] Prepared 1 package in [TIME] Installed 1 package in [TIME] - + package-a==1.2.3+foo - "#); + + package-a==1.2.3.post0 + "###); - // The version '1.2.3+foo' satisfies the constraint '>=1.2.3'. + // The version '1.2.3.post0' satisfies the constraint '==1.2.3.post0'. assert_installed( &context.venv, - "local_greater_than_or_equal_a", - "1.2.3+foo", + "post_equal_available_a", + "1.2.3.post0", &context.temp_dir, ); } -/// A local version should be excluded in exclusive ordered comparisons. +/// An equal version constraint should not match a post-release version if the post-release version is not available. /// /// ```text -/// local-less-than +/// post-equal-not-available /// ├── environment /// │ └── python3.8 /// ├── root -/// │ └── requires a<1.2.3 -/// │ └── unsatisfied: no matching version -/// └── a -/// └── a-1.2.3+foo -/// ``` -#[test] -fn local_less_than() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"local-less-than-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("local-less-than-a<1.2.3") - , @r#" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only package-a==1.2.3+foo is available and you require package-a<1.2.3, we can conclude that your requirements are unsatisfiable. - "#); - - assert_not_installed(&context.venv, "local_less_than_a", &context.temp_dir); -} - -/// A local version should be included in inclusive ordered comparisons. -/// -/// ```text -/// local-less-than-or-equal -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a<=1.2.3 -/// │ └── satisfied by a-1.2.3+foo -/// └── a -/// └── a-1.2.3+foo -/// ``` -#[test] -fn local_less_than_or_equal() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"local-less-than-or-equal-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("local-less-than-or-equal-a<=1.2.3") - , @r#" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only package-a==1.2.3+foo is available and you require package-a<=1.2.3, we can conclude that your requirements are unsatisfiable. - "#); - - // The version '1.2.3+foo' satisfies the constraint '<=1.2.3'. - assert_not_installed( - &context.venv, - "local_less_than_or_equal_a", - &context.temp_dir, - ); -} - -/// A simple version constraint should not match a post-release version. -/// -/// ```text -/// post-simple -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a==1.2.3 +/// │ └── requires a==1.2.3.post0 /// │ └── unsatisfied: no matching version /// └── a +/// ├── a-1.2.3 /// └── a-1.2.3.post1 /// ``` #[test] -fn post_simple() { +fn post_equal_not_available() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"post-simple-", "package-")); + filters.push((r"post-equal-not-available-", "package-")); uv_snapshot!(filters, command(&context) - .arg("post-simple-a==1.2.3") - , @r#" + .arg("post-equal-not-available-a==1.2.3.post0") + , @r###" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because there is no version of package-a==1.2.3 and you require package-a==1.2.3, we can conclude that your requirements are unsatisfiable. - "#); + ╰─▶ Because there is no version of package-a==1.2.3.post0 and you require package-a==1.2.3.post0, we can conclude that your requirements are unsatisfiable. + "###); - assert_not_installed(&context.venv, "post_simple_a", &context.temp_dir); + assert_not_installed( + &context.venv, + "post_equal_not_available_a", + &context.temp_dir, + ); +} + +/// A greater-than-or-equal version constraint should match a post-release version if the constraint is itself a post-release version. +/// +/// ```text +/// post-greater-than-or-equal-post +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a>=1.2.3.post0 +/// │ ├── satisfied by a-1.2.3.post0 +/// │ └── satisfied by a-1.2.3.post1 +/// └── a +/// ├── a-1.2.3.post0 +/// └── a-1.2.3.post1 +/// ``` +#[test] +fn post_greater_than_or_equal_post() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"post-greater-than-or-equal-post-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("post-greater-than-or-equal-post-a>=1.2.3.post0") + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + package-a==1.2.3.post1 + "###); + + // The version '1.2.3.post1' satisfies the constraint '>=1.2.3.post0'. + assert_installed( + &context.venv, + "post_greater_than_or_equal_post_a", + "1.2.3.post1", + &context.temp_dir, + ); } /// A greater-than-or-equal version constraint should match a post-release version. @@ -2123,7 +2179,7 @@ fn post_greater_than_or_equal() { uv_snapshot!(filters, command(&context) .arg("post-greater-than-or-equal-a>=1.2.3") - , @r#" + , @r###" success: true exit_code: 0 ----- stdout ----- @@ -2133,7 +2189,7 @@ fn post_greater_than_or_equal() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.2.3.post1 - "#); + "###); // The version '1.2.3.post1' satisfies the constraint '>=1.2.3'. assert_installed( @@ -2144,6 +2200,91 @@ fn post_greater_than_or_equal() { ); } +/// A greater-than version constraint should not match a post-release version if the post-release version is not available. +/// +/// ```text +/// post-greater-than-post-not-available +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a>1.2.3.post2 +/// │ └── unsatisfied: no matching version +/// └── a +/// ├── a-1.2.3 +/// ├── a-1.2.3.post0 +/// └── a-1.2.3.post1 +/// ``` +#[test] +fn post_greater_than_post_not_available() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"post-greater-than-post-not-available-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("post-greater-than-post-not-available-a>1.2.3.post2") + , @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because only package-a<=1.2.3.post1 is available and you require package-a>=1.2.3.post3, we can conclude that your requirements are unsatisfiable. + "###); + + assert_not_installed( + &context.venv, + "post_greater_than_post_not_available_a", + &context.temp_dir, + ); +} + +/// A greater-than version constraint should match a post-release version if the constraint is itself a post-release version. +/// +/// ```text +/// post-greater-than-post +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a>1.2.3.post0 +/// │ └── satisfied by a-1.2.3.post1 +/// └── a +/// ├── a-1.2.3.post0 +/// └── a-1.2.3.post1 +/// ``` +#[test] +fn post_greater_than_post() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"post-greater-than-post-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("post-greater-than-post-a>1.2.3.post0") + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + package-a==1.2.3.post1 + "###); + + // The version '1.2.3.post1' satisfies the constraint '>1.2.3.post0'. + assert_installed( + &context.venv, + "post_greater_than_post_a", + "1.2.3.post1", + &context.temp_dir, + ); +} + /// A greater-than version constraint should not match a post-release version. /// /// ```text @@ -2166,7 +2307,7 @@ fn post_greater_than() { uv_snapshot!(filters, command(&context) .arg("post-greater-than-a>1.2.3") - , @r#" + , @r###" success: false exit_code: 1 ----- stdout ----- @@ -2174,102 +2315,11 @@ fn post_greater_than() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because only package-a==1.2.3.post1 is available and you require package-a>1.2.3, we can conclude that your requirements are unsatisfiable. - "#); + "###); assert_not_installed(&context.venv, "post_greater_than_a", &context.temp_dir); } -/// A greater-than version constraint should match a post-release version if the -/// constraint is itself a post-release version. -/// -/// ```text -/// post-greater-than-post -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a>1.2.3.post0 -/// │ └── satisfied by a-1.2.3.post1 -/// └── a -/// ├── a-1.2.3.post0 -/// └── a-1.2.3.post1 -/// ``` -#[test] -fn post_greater_than_post() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"post-greater-than-post-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("post-greater-than-post-a>1.2.3.post0") - , @r#" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Prepared 1 package in [TIME] - Installed 1 package in [TIME] - + package-a==1.2.3.post1 - "#); - - // The version '1.2.3.post1' satisfies the constraint '>1.2.3.post0'. - assert_installed( - &context.venv, - "post_greater_than_post_a", - "1.2.3.post1", - &context.temp_dir, - ); -} - -/// A greater-than-or-equal version constraint should match a post-release version -/// if the constraint is itself a post-release version. -/// -/// ```text -/// post-greater-than-or-equal-post -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a>=1.2.3.post0 -/// │ ├── satisfied by a-1.2.3.post0 -/// │ └── satisfied by a-1.2.3.post1 -/// └── a -/// ├── a-1.2.3.post0 -/// └── a-1.2.3.post1 -/// ``` -#[test] -fn post_greater_than_or_equal_post() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"post-greater-than-or-equal-post-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("post-greater-than-or-equal-post-a>=1.2.3.post0") - , @r#" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Prepared 1 package in [TIME] - Installed 1 package in [TIME] - + package-a==1.2.3.post1 - "#); - - // The version '1.2.3.post1' satisfies the constraint '>=1.2.3.post0'. - assert_installed( - &context.venv, - "post_greater_than_or_equal_post_a", - "1.2.3.post1", - &context.temp_dir, - ); -} - /// A less-than-or-equal version constraint should not match a post-release version. /// /// ```text @@ -2292,7 +2342,7 @@ fn post_less_than_or_equal() { uv_snapshot!(filters, command(&context) .arg("post-less-than-or-equal-a<=1.2.3") - , @r#" + , @r###" success: false exit_code: 1 ----- stdout ----- @@ -2300,7 +2350,7 @@ fn post_less_than_or_equal() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because only package-a==1.2.3.post1 is available and you require package-a<=1.2.3, we can conclude that your requirements are unsatisfiable. - "#); + "###); assert_not_installed( &context.venv, @@ -2331,7 +2381,7 @@ fn post_less_than() { uv_snapshot!(filters, command(&context) .arg("post-less-than-a<1.2.3") - , @r#" + , @r###" success: false exit_code: 1 ----- stdout ----- @@ -2339,54 +2389,12 @@ fn post_less_than() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because only package-a==1.2.3.post1 is available and you require package-a<1.2.3, we can conclude that your requirements are unsatisfiable. - "#); + "###); assert_not_installed(&context.venv, "post_less_than_a", &context.temp_dir); } -/// A greater-than version constraint should not match a post-release version with a -/// local version identifier. -/// -/// ```text -/// post-local-greater-than -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a>1.2.3 -/// │ └── unsatisfied: no matching version -/// └── a -/// ├── a-1.2.3.post1 -/// └── a-1.2.3.post1+local -/// ``` -#[test] -fn post_local_greater_than() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"post-local-greater-than-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("post-local-greater-than-a>1.2.3") - , @r#" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only package-a<=1.2.3.post1+local is available and you require package-a>1.2.3, we can conclude that your requirements are unsatisfiable. - "#); - - assert_not_installed( - &context.venv, - "post_local_greater_than_a", - &context.temp_dir, - ); -} - -/// A greater-than version constraint should not match a post-release version with a -/// local version identifier. +/// A greater-than version constraint should not match a post-release version with a local version identifier. /// /// ```text /// post-local-greater-than-post @@ -2409,7 +2417,7 @@ fn post_local_greater_than_post() { uv_snapshot!(filters, command(&context) .arg("post-local-greater-than-post-a>1.2.3.post1") - , @r#" + , @r###" success: false exit_code: 1 ----- stdout ----- @@ -2417,7 +2425,7 @@ fn post_local_greater_than_post() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because only package-a<=1.2.3.post1+local is available and you require package-a>=1.2.3.post2, we can conclude that your requirements are unsatisfiable. - "#); + "###); assert_not_installed( &context.venv, @@ -2426,474 +2434,82 @@ fn post_local_greater_than_post() { ); } -/// An equal version constraint should not match a post-release version if the post- -/// release version is not available. +/// A greater-than version constraint should not match a post-release version with a local version identifier. /// /// ```text -/// post-equal-not-available +/// post-local-greater-than /// ├── environment /// │ └── python3.8 /// ├── root -/// │ └── requires a==1.2.3.post0 +/// │ └── requires a>1.2.3 +/// │ └── unsatisfied: no matching version +/// └── a +/// ├── a-1.2.3.post1 +/// └── a-1.2.3.post1+local +/// ``` +#[test] +fn post_local_greater_than() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"post-local-greater-than-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("post-local-greater-than-a>1.2.3") + , @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because only package-a<=1.2.3.post1+local is available and you require package-a>1.2.3, we can conclude that your requirements are unsatisfiable. + "###); + + assert_not_installed( + &context.venv, + "post_local_greater_than_a", + &context.temp_dir, + ); +} + +/// A simple version constraint should not match a post-release version. +/// +/// ```text +/// post-simple +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a==1.2.3 /// │ └── unsatisfied: no matching version /// └── a -/// ├── a-1.2.3 /// └── a-1.2.3.post1 /// ``` #[test] -fn post_equal_not_available() { +fn post_simple() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"post-equal-not-available-", "package-")); + filters.push((r"post-simple-", "package-")); uv_snapshot!(filters, command(&context) - .arg("post-equal-not-available-a==1.2.3.post0") - , @r#" + .arg("post-simple-a==1.2.3") + , @r###" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because there is no version of package-a==1.2.3.post0 and you require package-a==1.2.3.post0, we can conclude that your requirements are unsatisfiable. - "#); + ╰─▶ Because there is no version of package-a==1.2.3 and you require package-a==1.2.3, we can conclude that your requirements are unsatisfiable. + "###); - assert_not_installed( - &context.venv, - "post_equal_not_available_a", - &context.temp_dir, - ); + assert_not_installed(&context.venv, "post_simple_a", &context.temp_dir); } -/// An equal version constraint should match a post-release version if the post- -/// release version is available. -/// -/// ```text -/// post-equal-available -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a==1.2.3.post0 -/// │ └── satisfied by a-1.2.3.post0 -/// └── a -/// ├── a-1.2.3 -/// └── a-1.2.3.post0 -/// ``` -#[test] -fn post_equal_available() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"post-equal-available-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("post-equal-available-a==1.2.3.post0") - , @r#" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Prepared 1 package in [TIME] - Installed 1 package in [TIME] - + package-a==1.2.3.post0 - "#); - - // The version '1.2.3.post0' satisfies the constraint '==1.2.3.post0'. - assert_installed( - &context.venv, - "post_equal_available_a", - "1.2.3.post0", - &context.temp_dir, - ); -} - -/// A greater-than version constraint should not match a post-release version if the -/// post-release version is not available. -/// -/// ```text -/// post-greater-than-post-not-available -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a>1.2.3.post2 -/// │ └── unsatisfied: no matching version -/// └── a -/// ├── a-1.2.3 -/// ├── a-1.2.3.post0 -/// └── a-1.2.3.post1 -/// ``` -#[test] -fn post_greater_than_post_not_available() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"post-greater-than-post-not-available-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("post-greater-than-post-not-available-a>1.2.3.post2") - , @r#" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only package-a<=1.2.3.post1 is available and you require package-a>=1.2.3.post3, we can conclude that your requirements are unsatisfiable. - "#); - - assert_not_installed( - &context.venv, - "post_greater_than_post_not_available_a", - &context.temp_dir, - ); -} - -/// The user requires any version of package `a` which only has prerelease versions -/// available. -/// -/// ```text -/// package-only-prereleases -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a -/// │ └── unsatisfied: no matching version -/// └── a -/// └── a-1.0.0a1 -/// ``` -#[test] -fn package_only_prereleases() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"package-only-prereleases-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("package-only-prereleases-a") - , @r#" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Prepared 1 package in [TIME] - Installed 1 package in [TIME] - + package-a==1.0.0a1 - "#); - - // Since there are only prerelease versions of `a` available, it should be - // installed even though the user did not include a prerelease specifier. - assert_installed( - &context.venv, - "package_only_prereleases_a", - "1.0.0a1", - &context.temp_dir, - ); -} - -/// The user requires a version of package `a` which only matches prerelease -/// versions but they did not include a prerelease specifier. -/// -/// ```text -/// package-only-prereleases-in-range -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a>0.1.0 -/// │ └── unsatisfied: no matching version -/// └── a -/// ├── a-0.1.0 -/// └── a-1.0.0a1 -/// ``` -#[test] -fn package_only_prereleases_in_range() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"package-only-prereleases-in-range-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("package-only-prereleases-in-range-a>0.1.0") - , @r#" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only package-a<0.1.0 is available and you require package-a>0.1.0, we can conclude that your requirements are unsatisfiable. - - hint: Pre-releases are available for package-a in the requested range (e.g., 1.0.0a1), but pre-releases weren't enabled (try: `--prerelease=allow`) - "#); - - // Since there are stable versions of `a` available, prerelease versions should not - // be selected without explicit opt-in. - assert_not_installed( - &context.venv, - "package_only_prereleases_in_range_a", - &context.temp_dir, - ); -} - -/// The user requires a version of package `a` which only matches prerelease -/// versions. They did not include a prerelease specifier for the package, but they -/// opted into prereleases globally. -/// -/// ```text -/// requires-package-only-prereleases-in-range-global-opt-in -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a>0.1.0 -/// │ └── unsatisfied: no matching version -/// └── a -/// ├── a-0.1.0 -/// └── a-1.0.0a1 -/// ``` -#[test] -fn requires_package_only_prereleases_in_range_global_opt_in() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push(( - r"requires-package-only-prereleases-in-range-global-opt-in-", - "package-", - )); - - uv_snapshot!(filters, command(&context) - .arg("--prerelease=allow") - .arg("requires-package-only-prereleases-in-range-global-opt-in-a>0.1.0") - , @r#" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Prepared 1 package in [TIME] - Installed 1 package in [TIME] - + package-a==1.0.0a1 - "#); - - assert_installed( - &context.venv, - "requires_package_only_prereleases_in_range_global_opt_in_a", - "1.0.0a1", - &context.temp_dir, - ); -} - -/// The user requires any version of package `a` has a prerelease version available -/// and an older non-prerelease version. -/// -/// ```text -/// requires-package-prerelease-and-final-any -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a -/// │ └── satisfied by a-0.1.0 -/// └── a -/// ├── a-0.1.0 -/// └── a-1.0.0a1 -/// ``` -#[test] -fn requires_package_prerelease_and_final_any() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"requires-package-prerelease-and-final-any-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("requires-package-prerelease-and-final-any-a") - , @r#" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Prepared 1 package in [TIME] - Installed 1 package in [TIME] - + package-a==0.1.0 - "#); - - // Since the user did not provide a prerelease specifier, the older stable version - // should be selected. - assert_installed( - &context.venv, - "requires_package_prerelease_and_final_any_a", - "0.1.0", - &context.temp_dir, - ); -} - -/// The user requires a version of `a` with a prerelease specifier and only stable -/// releases are available. -/// -/// ```text -/// package-prerelease-specified-only-final-available -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a>=0.1.0a1 -/// │ ├── satisfied by a-0.1.0 -/// │ ├── satisfied by a-0.2.0 -/// │ └── satisfied by a-0.3.0 -/// └── a -/// ├── a-0.1.0 -/// ├── a-0.2.0 -/// └── a-0.3.0 -/// ``` -#[test] -fn package_prerelease_specified_only_final_available() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push(( - r"package-prerelease-specified-only-final-available-", - "package-", - )); - - uv_snapshot!(filters, command(&context) - .arg("package-prerelease-specified-only-final-available-a>=0.1.0a1") - , @r#" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Prepared 1 package in [TIME] - Installed 1 package in [TIME] - + package-a==0.3.0 - "#); - - // The latest stable version should be selected. - assert_installed( - &context.venv, - "package_prerelease_specified_only_final_available_a", - "0.3.0", - &context.temp_dir, - ); -} - -/// The user requires a version of `a` with a prerelease specifier and only -/// prerelease releases are available. -/// -/// ```text -/// package-prerelease-specified-only-prerelease-available -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a>=0.1.0a1 -/// │ ├── satisfied by a-0.1.0a1 -/// │ ├── satisfied by a-0.2.0a1 -/// │ └── satisfied by a-0.3.0a1 -/// └── a -/// ├── a-0.1.0a1 -/// ├── a-0.2.0a1 -/// └── a-0.3.0a1 -/// ``` -#[test] -fn package_prerelease_specified_only_prerelease_available() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push(( - r"package-prerelease-specified-only-prerelease-available-", - "package-", - )); - - uv_snapshot!(filters, command(&context) - .arg("package-prerelease-specified-only-prerelease-available-a>=0.1.0a1") - , @r#" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Prepared 1 package in [TIME] - Installed 1 package in [TIME] - + package-a==0.3.0a1 - "#); - - // The latest prerelease version should be selected. - assert_installed( - &context.venv, - "package_prerelease_specified_only_prerelease_available_a", - "0.3.0a1", - &context.temp_dir, - ); -} - -/// The user requires a version of `a` with a prerelease specifier and both -/// prerelease and stable releases are available. -/// -/// ```text -/// package-prerelease-specified-mixed-available -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a>=0.1.0a1 -/// │ ├── satisfied by a-0.1.0 -/// │ ├── satisfied by a-0.2.0a1 -/// │ ├── satisfied by a-0.3.0 -/// │ └── satisfied by a-1.0.0a1 -/// └── a -/// ├── a-0.1.0 -/// ├── a-0.2.0a1 -/// ├── a-0.3.0 -/// └── a-1.0.0a1 -/// ``` -#[test] -fn package_prerelease_specified_mixed_available() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"package-prerelease-specified-mixed-available-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("package-prerelease-specified-mixed-available-a>=0.1.0a1") - , @r#" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Prepared 1 package in [TIME] - Installed 1 package in [TIME] - + package-a==1.0.0a1 - "#); - - // Since the user provided a prerelease specifier, the latest prerelease version - // should be selected. - assert_installed( - &context.venv, - "package_prerelease_specified_mixed_available_a", - "1.0.0a1", - &context.temp_dir, - ); -} - -/// The user requires `a` which has multiple prereleases available with different -/// labels. +/// The user requires `a` which has multiple prereleases available with different labels. /// /// ```text /// package-multiple-prereleases-kinds @@ -2919,7 +2535,7 @@ fn package_multiple_prereleases_kinds() { uv_snapshot!(filters, command(&context) .arg("package-multiple-prereleases-kinds-a>=1.0.0a1") - , @r#" + , @r###" success: true exit_code: 0 ----- stdout ----- @@ -2929,7 +2545,7 @@ fn package_multiple_prereleases_kinds() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.0.0rc1 - "#); + "###); // Release candidates should be the highest precedence prerelease kind. assert_installed( @@ -2966,7 +2582,7 @@ fn package_multiple_prereleases_numbers() { uv_snapshot!(filters, command(&context) .arg("package-multiple-prereleases-numbers-a>=1.0.0a1") - , @r#" + , @r###" success: true exit_code: 0 ----- stdout ----- @@ -2976,7 +2592,7 @@ fn package_multiple_prereleases_numbers() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.0.0a3 - "#); + "###); // The latest alpha version should be selected. assert_installed( @@ -2987,116 +2603,518 @@ fn package_multiple_prereleases_numbers() { ); } -/// The user requires any version of package `a` which requires `b` which only has -/// prerelease versions available. +/// The user requires a non-prerelease version of `a` which only has prerelease versions available. There are pre-releases on the boundary of their range. /// /// ```text -/// transitive-package-only-prereleases +/// package-only-prereleases-boundary /// ├── environment /// │ └── python3.8 /// ├── root -/// │ └── requires a -/// │ └── satisfied by a-0.1.0 -/// ├── a -/// │ └── a-0.1.0 -/// │ └── requires b -/// │ └── unsatisfied: no matching version -/// └── b -/// └── b-1.0.0a1 +/// │ └── requires a<0.2.0 +/// │ └── unsatisfied: no matching version +/// └── a +/// ├── a-0.1.0a1 +/// ├── a-0.2.0a1 +/// └── a-0.3.0a1 /// ``` #[test] -fn transitive_package_only_prereleases() { +fn package_only_prereleases_boundary() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"transitive-package-only-prereleases-", "package-")); + filters.push((r"package-only-prereleases-boundary-", "package-")); uv_snapshot!(filters, command(&context) - .arg("transitive-package-only-prereleases-a") - , @r#" + .arg("package-only-prereleases-boundary-a<0.2.0") + , @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Resolved 2 packages in [TIME] - Prepared 2 packages in [TIME] - Installed 2 packages in [TIME] - + package-a==0.1.0 - + package-b==1.0.0a1 - "#); + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + package-a==0.1.0a1 + "###); - // Since there are only prerelease versions of `b` available, it should be selected - // even though the user did not opt-in to prereleases. + // Since there are only prerelease versions of `a` available, a prerelease is allowed. Since the user did not explicitly request a pre-release, pre-releases at the boundary should not be selected. assert_installed( &context.venv, - "transitive_package_only_prereleases_a", - "0.1.0", - &context.temp_dir, - ); - assert_installed( - &context.venv, - "transitive_package_only_prereleases_b", - "1.0.0a1", + "package_only_prereleases_boundary_a", + "0.1.0a1", &context.temp_dir, ); } -/// The user requires package `a` which has a dependency on a package which only -/// matches prerelease versions but they did not include a prerelease specifier. +/// The user requires a version of package `a` which only matches prerelease versions but they did not include a prerelease specifier. /// /// ```text -/// transitive-package-only-prereleases-in-range +/// package-only-prereleases-in-range /// ├── environment /// │ └── python3.8 /// ├── root -/// │ └── requires a -/// │ └── satisfied by a-0.1.0 -/// ├── a -/// │ └── a-0.1.0 -/// │ └── requires b>0.1 -/// │ └── unsatisfied: no matching version -/// └── b -/// ├── b-0.1.0 -/// └── b-1.0.0a1 +/// │ └── requires a>0.1.0 +/// │ └── unsatisfied: no matching version +/// └── a +/// ├── a-0.1.0 +/// └── a-1.0.0a1 /// ``` #[test] -fn transitive_package_only_prereleases_in_range() { +fn package_only_prereleases_in_range() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"transitive-package-only-prereleases-in-range-", "package-")); + filters.push((r"package-only-prereleases-in-range-", "package-")); uv_snapshot!(filters, command(&context) - .arg("transitive-package-only-prereleases-in-range-a") - , @r#" + .arg("package-only-prereleases-in-range-a>0.1.0") + , @r###" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because only package-b<0.1 is available and package-a==0.1.0 depends on package-b>0.1, we can conclude that package-a==0.1.0 cannot be used. - And because only package-a==0.1.0 is available and you require package-a, we can conclude that your requirements are unsatisfiable. + ╰─▶ Because only package-a<0.1.0 is available and you require package-a>0.1.0, we can conclude that your requirements are unsatisfiable. - hint: Pre-releases are available for package-b in the requested range (e.g., 1.0.0a1), but pre-releases weren't enabled (try: `--prerelease=allow`) - "#); + hint: Pre-releases are available for package-a in the requested range (e.g., 1.0.0a1), but pre-releases weren't enabled (try: `--prerelease=allow`) + "###); - // Since there are stable versions of `b` available, the prerelease version should - // not be selected without explicit opt-in. The available version is excluded by - // the range requested by the user. + // Since there are stable versions of `a` available, prerelease versions should not be selected without explicit opt-in. assert_not_installed( &context.venv, - "transitive_package_only_prereleases_in_range_a", + "package_only_prereleases_in_range_a", &context.temp_dir, ); } -/// The user requires package `a` which has a dependency on a package which only -/// matches prerelease versions; the user has opted into allowing prereleases in `b` -/// explicitly. +/// The user requires any version of package `a` which only has prerelease versions available. +/// +/// ```text +/// package-only-prereleases +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a +/// │ └── unsatisfied: no matching version +/// └── a +/// └── a-1.0.0a1 +/// ``` +#[test] +fn package_only_prereleases() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"package-only-prereleases-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("package-only-prereleases-a") + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + package-a==1.0.0a1 + "###); + + // Since there are only prerelease versions of `a` available, it should be installed even though the user did not include a prerelease specifier. + assert_installed( + &context.venv, + "package_only_prereleases_a", + "1.0.0a1", + &context.temp_dir, + ); +} + +/// The user requires a version of `a` with a prerelease specifier and both prerelease and stable releases are available. +/// +/// ```text +/// package-prerelease-specified-mixed-available +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a>=0.1.0a1 +/// │ ├── satisfied by a-0.1.0 +/// │ ├── satisfied by a-0.2.0a1 +/// │ ├── satisfied by a-0.3.0 +/// │ └── satisfied by a-1.0.0a1 +/// └── a +/// ├── a-0.1.0 +/// ├── a-0.2.0a1 +/// ├── a-0.3.0 +/// └── a-1.0.0a1 +/// ``` +#[test] +fn package_prerelease_specified_mixed_available() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"package-prerelease-specified-mixed-available-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("package-prerelease-specified-mixed-available-a>=0.1.0a1") + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + package-a==1.0.0a1 + "###); + + // Since the user provided a prerelease specifier, the latest prerelease version should be selected. + assert_installed( + &context.venv, + "package_prerelease_specified_mixed_available_a", + "1.0.0a1", + &context.temp_dir, + ); +} + +/// The user requires a version of `a` with a prerelease specifier and only stable releases are available. +/// +/// ```text +/// package-prerelease-specified-only-final-available +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a>=0.1.0a1 +/// │ ├── satisfied by a-0.1.0 +/// │ ├── satisfied by a-0.2.0 +/// │ └── satisfied by a-0.3.0 +/// └── a +/// ├── a-0.1.0 +/// ├── a-0.2.0 +/// └── a-0.3.0 +/// ``` +#[test] +fn package_prerelease_specified_only_final_available() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push(( + r"package-prerelease-specified-only-final-available-", + "package-", + )); + + uv_snapshot!(filters, command(&context) + .arg("package-prerelease-specified-only-final-available-a>=0.1.0a1") + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + package-a==0.3.0 + "###); + + // The latest stable version should be selected. + assert_installed( + &context.venv, + "package_prerelease_specified_only_final_available_a", + "0.3.0", + &context.temp_dir, + ); +} + +/// The user requires a version of `a` with a prerelease specifier and only prerelease releases are available. +/// +/// ```text +/// package-prerelease-specified-only-prerelease-available +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a>=0.1.0a1 +/// │ ├── satisfied by a-0.1.0a1 +/// │ ├── satisfied by a-0.2.0a1 +/// │ └── satisfied by a-0.3.0a1 +/// └── a +/// ├── a-0.1.0a1 +/// ├── a-0.2.0a1 +/// └── a-0.3.0a1 +/// ``` +#[test] +fn package_prerelease_specified_only_prerelease_available() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push(( + r"package-prerelease-specified-only-prerelease-available-", + "package-", + )); + + uv_snapshot!(filters, command(&context) + .arg("package-prerelease-specified-only-prerelease-available-a>=0.1.0a1") + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + package-a==0.3.0a1 + "###); + + // The latest prerelease version should be selected. + assert_installed( + &context.venv, + "package_prerelease_specified_only_prerelease_available_a", + "0.3.0a1", + &context.temp_dir, + ); +} + +/// The user requires a non-prerelease version of `a` but has enabled pre-releases. There are pre-releases on the boundary of their range. +/// +/// ```text +/// package-prereleases-boundary +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a<0.2.0 +/// │ └── satisfied by a-0.1.0 +/// └── a +/// ├── a-0.1.0 +/// ├── a-0.2.0a1 +/// └── a-0.3.0 +/// ``` +#[test] +fn package_prereleases_boundary() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"package-prereleases-boundary-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("--prerelease=allow") + .arg("package-prereleases-boundary-a<0.2.0") + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + package-a==0.1.0 + "###); + + // Since the user did not use a pre-release specifier, pre-releases at the boundary should not be selected even though pre-releases are allowed. + assert_installed( + &context.venv, + "package_prereleases_boundary_a", + "0.1.0", + &context.temp_dir, + ); +} + +/// The user requires a non-prerelease version of `a` but has enabled pre-releases. There are pre-releases on the boundary of their range. +/// +/// ```text +/// package-prereleases-global-boundary +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a<0.2.0 +/// │ └── satisfied by a-0.1.0 +/// └── a +/// ├── a-0.1.0 +/// ├── a-0.2.0a1 +/// └── a-0.3.0 +/// ``` +#[test] +fn package_prereleases_global_boundary() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"package-prereleases-global-boundary-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("--prerelease=allow") + .arg("package-prereleases-global-boundary-a<0.2.0") + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + package-a==0.1.0 + "###); + + // Since the user did not use a pre-release specifier, pre-releases at the boundary should not be selected even though pre-releases are allowed. + assert_installed( + &context.venv, + "package_prereleases_global_boundary_a", + "0.1.0", + &context.temp_dir, + ); +} + +/// The user requires a prerelease version of `a`. There are pre-releases on the boundary of their range. +/// +/// ```text +/// package-prereleases-specifier-boundary +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a<0.2.0a2 +/// │ └── satisfied by a-0.1.0 +/// └── a +/// ├── a-0.1.0 +/// ├── a-0.2.0 +/// ├── a-0.2.0a1 +/// ├── a-0.2.0a2 +/// ├── a-0.2.0a3 +/// └── a-0.3.0 +/// ``` +#[test] +fn package_prereleases_specifier_boundary() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"package-prereleases-specifier-boundary-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("package-prereleases-specifier-boundary-a<0.2.0a2") + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + package-a==0.2.0a1 + "###); + + // Since the user used a pre-release specifier, pre-releases at the boundary should be selected. + assert_installed( + &context.venv, + "package_prereleases_specifier_boundary_a", + "0.2.0a1", + &context.temp_dir, + ); +} + +/// The user requires a version of package `a` which only matches prerelease versions. They did not include a prerelease specifier for the package, but they opted into prereleases globally. +/// +/// ```text +/// requires-package-only-prereleases-in-range-global-opt-in +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a>0.1.0 +/// │ └── unsatisfied: no matching version +/// └── a +/// ├── a-0.1.0 +/// └── a-1.0.0a1 +/// ``` +#[test] +fn requires_package_only_prereleases_in_range_global_opt_in() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push(( + r"requires-package-only-prereleases-in-range-global-opt-in-", + "package-", + )); + + uv_snapshot!(filters, command(&context) + .arg("--prerelease=allow") + .arg("requires-package-only-prereleases-in-range-global-opt-in-a>0.1.0") + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + package-a==1.0.0a1 + "###); + + assert_installed( + &context.venv, + "requires_package_only_prereleases_in_range_global_opt_in_a", + "1.0.0a1", + &context.temp_dir, + ); +} + +/// The user requires any version of package `a` has a prerelease version available and an older non-prerelease version. +/// +/// ```text +/// requires-package-prerelease-and-final-any +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a +/// │ └── satisfied by a-0.1.0 +/// └── a +/// ├── a-0.1.0 +/// └── a-1.0.0a1 +/// ``` +#[test] +fn requires_package_prerelease_and_final_any() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"requires-package-prerelease-and-final-any-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("requires-package-prerelease-and-final-any-a") + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + package-a==0.1.0 + "###); + + // Since the user did not provide a prerelease specifier, the older stable version should be selected. + assert_installed( + &context.venv, + "requires_package_prerelease_and_final_any_a", + "0.1.0", + &context.temp_dir, + ); +} + +/// The user requires package `a` which has a dependency on a package which only matches prerelease versions; the user has opted into allowing prereleases in `b` explicitly. /// /// ```text /// transitive-package-only-prereleases-in-range-opt-in @@ -3129,7 +3147,7 @@ fn transitive_package_only_prereleases_in_range_opt_in() { uv_snapshot!(filters, command(&context) .arg("transitive-package-only-prereleases-in-range-opt-in-a") .arg("transitive-package-only-prereleases-in-range-opt-in-b>0.0.0a1") - , @r#" + , @r###" success: true exit_code: 0 ----- stdout ----- @@ -3140,10 +3158,9 @@ fn transitive_package_only_prereleases_in_range_opt_in() { Installed 2 packages in [TIME] + package-a==0.1.0 + package-b==1.0.0a1 - "#); + "###); - // Since the user included a dependency on `b` with a prerelease specifier, a - // prerelease version can be selected. + // Since the user included a dependency on `b` with a prerelease specifier, a prerelease version can be selected. assert_installed( &context.venv, "transitive_package_only_prereleases_in_range_opt_in_a", @@ -3158,11 +3175,112 @@ fn transitive_package_only_prereleases_in_range_opt_in() { ); } -/// A transitive dependency has both a prerelease and a stable selector, but can -/// only be satisfied by a prerelease +/// The user requires package `a` which has a dependency on a package which only matches prerelease versions but they did not include a prerelease specifier. /// /// ```text -/// transitive-prerelease-and-stable-dependency +/// transitive-package-only-prereleases-in-range +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a +/// │ └── satisfied by a-0.1.0 +/// ├── a +/// │ └── a-0.1.0 +/// │ └── requires b>0.1 +/// │ └── unsatisfied: no matching version +/// └── b +/// ├── b-0.1.0 +/// └── b-1.0.0a1 +/// ``` +#[test] +fn transitive_package_only_prereleases_in_range() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"transitive-package-only-prereleases-in-range-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("transitive-package-only-prereleases-in-range-a") + , @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because only package-b<0.1 is available and package-a==0.1.0 depends on package-b>0.1, we can conclude that package-a==0.1.0 cannot be used. + And because only package-a==0.1.0 is available and you require package-a, we can conclude that your requirements are unsatisfiable. + + hint: Pre-releases are available for package-b in the requested range (e.g., 1.0.0a1), but pre-releases weren't enabled (try: `--prerelease=allow`) + "###); + + // Since there are stable versions of `b` available, the prerelease version should not be selected without explicit opt-in. The available version is excluded by the range requested by the user. + assert_not_installed( + &context.venv, + "transitive_package_only_prereleases_in_range_a", + &context.temp_dir, + ); +} + +/// The user requires any version of package `a` which requires `b` which only has prerelease versions available. +/// +/// ```text +/// transitive-package-only-prereleases +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a +/// │ └── satisfied by a-0.1.0 +/// ├── a +/// │ └── a-0.1.0 +/// │ └── requires b +/// │ └── unsatisfied: no matching version +/// └── b +/// └── b-1.0.0a1 +/// ``` +#[test] +fn transitive_package_only_prereleases() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"transitive-package-only-prereleases-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("transitive-package-only-prereleases-a") + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + package-a==0.1.0 + + package-b==1.0.0a1 + "###); + + // Since there are only prerelease versions of `b` available, it should be selected even though the user did not opt-in to prereleases. + assert_installed( + &context.venv, + "transitive_package_only_prereleases_a", + "0.1.0", + &context.temp_dir, + ); + assert_installed( + &context.venv, + "transitive_package_only_prereleases_b", + "1.0.0a1", + &context.temp_dir, + ); +} + +/// A transitive dependency has both a prerelease and a stable selector, but can only be satisfied by a prerelease. There are many prerelease versions and some are excluded. +/// +/// ```text +/// transitive-prerelease-and-stable-dependency-many-versions-holes /// ├── environment /// │ └── python3.8 /// ├── root @@ -3172,133 +3290,87 @@ fn transitive_package_only_prereleases_in_range_opt_in() { /// │ └── satisfied by b-1.0.0 /// ├── a /// │ └── a-1.0.0 -/// │ └── requires c==2.0.0b1 -/// │ └── satisfied by c-2.0.0b1 +/// │ └── requires c!=2.0.0a5,!=2.0.0a6,!=2.0.0a7,!=2.0.0b1,<2.0.0b5,>1.0.0 +/// │ └── unsatisfied: no matching version /// ├── b /// │ └── b-1.0.0 /// │ └── requires c<=3.0.0,>=1.0.0 /// │ └── satisfied by c-1.0.0 /// └── c /// ├── c-1.0.0 -/// └── c-2.0.0b1 +/// ├── c-2.0.0a1 +/// ├── c-2.0.0a2 +/// ├── c-2.0.0a3 +/// ├── c-2.0.0a4 +/// ├── c-2.0.0a5 +/// ├── c-2.0.0a6 +/// ├── c-2.0.0a7 +/// ├── c-2.0.0a8 +/// ├── c-2.0.0a9 +/// ├── c-2.0.0b1 +/// ├── c-2.0.0b2 +/// ├── c-2.0.0b3 +/// ├── c-2.0.0b4 +/// ├── c-2.0.0b5 +/// ├── c-2.0.0b6 +/// ├── c-2.0.0b7 +/// ├── c-2.0.0b8 +/// └── c-2.0.0b9 /// ``` #[test] -fn transitive_prerelease_and_stable_dependency() { +fn transitive_prerelease_and_stable_dependency_many_versions_holes() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"transitive-prerelease-and-stable-dependency-", "package-")); + filters.push(( + r"transitive-prerelease-and-stable-dependency-many-versions-holes-", + "package-", + )); uv_snapshot!(filters, command(&context) - .arg("transitive-prerelease-and-stable-dependency-a") - .arg("transitive-prerelease-and-stable-dependency-b") - , @r#" + .arg("transitive-prerelease-and-stable-dependency-many-versions-holes-a") + .arg("transitive-prerelease-and-stable-dependency-many-versions-holes-b") + , @r###" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because there is no version of package-c==2.0.0b1 and package-a==1.0.0 depends on package-c==2.0.0b1, we can conclude that package-a==1.0.0 cannot be used. + ╰─▶ Because only the following versions of package-c are available: + package-c<1.0.0 + package-c>=2.0.0a5,<=2.0.0a7 + package-c==2.0.0b1 + package-c>=2.0.0b5 + and package-a==1.0.0 depends on one of: + package-c>1.0.0,<2.0.0a5 + package-c>2.0.0a7,<2.0.0b1 + package-c>2.0.0b1,<2.0.0b5 + we can conclude that package-a==1.0.0 cannot be used. And because only package-a==1.0.0 is available and you require package-a, we can conclude that your requirements are unsatisfiable. - hint: package-c was requested with a pre-release marker (e.g., package-c==2.0.0b1), but pre-releases weren't enabled (try: `--prerelease=allow`) - "#); + hint: package-c was requested with a pre-release marker (e.g., all of: + package-c>1.0.0,<2.0.0a5 + package-c>2.0.0a7,<2.0.0b1 + package-c>2.0.0b1,<2.0.0b5 + ), but pre-releases weren't enabled (try: `--prerelease=allow`) + "###); // Since the user did not explicitly opt-in to a prerelease, it cannot be selected. assert_not_installed( &context.venv, - "transitive_prerelease_and_stable_dependency_a", + "transitive_prerelease_and_stable_dependency_many_versions_holes_a", &context.temp_dir, ); assert_not_installed( &context.venv, - "transitive_prerelease_and_stable_dependency_b", + "transitive_prerelease_and_stable_dependency_many_versions_holes_b", &context.temp_dir, ); } -/// A transitive dependency has both a prerelease and a stable selector, but can -/// only be satisfied by a prerelease. The user includes an opt-in to prereleases of -/// the transitive dependency. -/// -/// ```text -/// transitive-prerelease-and-stable-dependency-opt-in -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ ├── requires a -/// │ │ └── satisfied by a-1.0.0 -/// │ ├── requires b -/// │ │ └── satisfied by b-1.0.0 -/// │ └── requires c>=0.0.0a1 -/// │ ├── satisfied by c-1.0.0 -/// │ └── satisfied by c-2.0.0b1 -/// ├── a -/// │ └── a-1.0.0 -/// │ └── requires c==2.0.0b1 -/// │ └── satisfied by c-2.0.0b1 -/// ├── b -/// │ └── b-1.0.0 -/// │ └── requires c<=3.0.0,>=1.0.0 -/// │ └── satisfied by c-1.0.0 -/// └── c -/// ├── c-1.0.0 -/// └── c-2.0.0b1 -/// ``` -#[test] -fn transitive_prerelease_and_stable_dependency_opt_in() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push(( - r"transitive-prerelease-and-stable-dependency-opt-in-", - "package-", - )); - - uv_snapshot!(filters, command(&context) - .arg("transitive-prerelease-and-stable-dependency-opt-in-a") - .arg("transitive-prerelease-and-stable-dependency-opt-in-b") - .arg("transitive-prerelease-and-stable-dependency-opt-in-c>=0.0.0a1") - , @r#" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 3 packages in [TIME] - Prepared 3 packages in [TIME] - Installed 3 packages in [TIME] - + package-a==1.0.0 - + package-b==1.0.0 - + package-c==2.0.0b1 - "#); - - // Since the user explicitly opted-in to a prerelease for `c`, it can be installed. - assert_installed( - &context.venv, - "transitive_prerelease_and_stable_dependency_opt_in_a", - "1.0.0", - &context.temp_dir, - ); - assert_installed( - &context.venv, - "transitive_prerelease_and_stable_dependency_opt_in_b", - "1.0.0", - &context.temp_dir, - ); - assert_installed( - &context.venv, - "transitive_prerelease_and_stable_dependency_opt_in_c", - "2.0.0b1", - &context.temp_dir, - ); -} - -/// A transitive dependency has both a prerelease and a stable selector, but can -/// only be satisfied by a prerelease. There are many prerelease versions. +/// A transitive dependency has both a prerelease and a stable selector, but can only be satisfied by a prerelease. There are many prerelease versions. /// /// ```text /// transitive-prerelease-and-stable-dependency-many-versions @@ -3360,7 +3432,7 @@ fn transitive_prerelease_and_stable_dependency_many_versions() { uv_snapshot!(filters, command(&context) .arg("transitive-prerelease-and-stable-dependency-many-versions-a") .arg("transitive-prerelease-and-stable-dependency-many-versions-b") - , @r#" + , @r###" success: false exit_code: 1 ----- stdout ----- @@ -3373,7 +3445,7 @@ fn transitive_prerelease_and_stable_dependency_many_versions() { And because you require package-a and package-b, we can conclude that your requirements are unsatisfiable. hint: package-c was requested with a pre-release marker (e.g., package-c>=2.0.0b1), but pre-releases weren't enabled (try: `--prerelease=allow`) - "#); + "###); // Since the user did not explicitly opt-in to a prerelease, it cannot be selected. assert_not_installed( @@ -3388,12 +3460,86 @@ fn transitive_prerelease_and_stable_dependency_many_versions() { ); } -/// A transitive dependency has both a prerelease and a stable selector, but can -/// only be satisfied by a prerelease. There are many prerelease versions and some -/// are excluded. +/// A transitive dependency has both a prerelease and a stable selector, but can only be satisfied by a prerelease. The user includes an opt-in to prereleases of the transitive dependency. /// /// ```text -/// transitive-prerelease-and-stable-dependency-many-versions-holes +/// transitive-prerelease-and-stable-dependency-opt-in +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ ├── requires a +/// │ │ └── satisfied by a-1.0.0 +/// │ ├── requires b +/// │ │ └── satisfied by b-1.0.0 +/// │ └── requires c>=0.0.0a1 +/// │ ├── satisfied by c-1.0.0 +/// │ └── satisfied by c-2.0.0b1 +/// ├── a +/// │ └── a-1.0.0 +/// │ └── requires c==2.0.0b1 +/// │ └── satisfied by c-2.0.0b1 +/// ├── b +/// │ └── b-1.0.0 +/// │ └── requires c<=3.0.0,>=1.0.0 +/// │ └── satisfied by c-1.0.0 +/// └── c +/// ├── c-1.0.0 +/// └── c-2.0.0b1 +/// ``` +#[test] +fn transitive_prerelease_and_stable_dependency_opt_in() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push(( + r"transitive-prerelease-and-stable-dependency-opt-in-", + "package-", + )); + + uv_snapshot!(filters, command(&context) + .arg("transitive-prerelease-and-stable-dependency-opt-in-a") + .arg("transitive-prerelease-and-stable-dependency-opt-in-b") + .arg("transitive-prerelease-and-stable-dependency-opt-in-c>=0.0.0a1") + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + package-a==1.0.0 + + package-b==1.0.0 + + package-c==2.0.0b1 + "###); + + // Since the user explicitly opted-in to a prerelease for `c`, it can be installed. + assert_installed( + &context.venv, + "transitive_prerelease_and_stable_dependency_opt_in_a", + "1.0.0", + &context.temp_dir, + ); + assert_installed( + &context.venv, + "transitive_prerelease_and_stable_dependency_opt_in_b", + "1.0.0", + &context.temp_dir, + ); + assert_installed( + &context.venv, + "transitive_prerelease_and_stable_dependency_opt_in_c", + "2.0.0b1", + &context.temp_dir, + ); +} + +/// A transitive dependency has both a prerelease and a stable selector, but can only be satisfied by a prerelease +/// +/// ```text +/// transitive-prerelease-and-stable-dependency /// ├── environment /// │ └── python3.8 /// ├── root @@ -3403,346 +3549,85 @@ fn transitive_prerelease_and_stable_dependency_many_versions() { /// │ └── satisfied by b-1.0.0 /// ├── a /// │ └── a-1.0.0 -/// │ └── requires c!=2.0.0a5,!=2.0.0a6,!=2.0.0a7,!=2.0.0b1,<2.0.0b5,>1.0.0 -/// │ └── unsatisfied: no matching version +/// │ └── requires c==2.0.0b1 +/// │ └── satisfied by c-2.0.0b1 /// ├── b /// │ └── b-1.0.0 /// │ └── requires c<=3.0.0,>=1.0.0 /// │ └── satisfied by c-1.0.0 /// └── c /// ├── c-1.0.0 -/// ├── c-2.0.0a1 -/// ├── c-2.0.0a2 -/// ├── c-2.0.0a3 -/// ├── c-2.0.0a4 -/// ├── c-2.0.0a5 -/// ├── c-2.0.0a6 -/// ├── c-2.0.0a7 -/// ├── c-2.0.0a8 -/// ├── c-2.0.0a9 -/// ├── c-2.0.0b1 -/// ├── c-2.0.0b2 -/// ├── c-2.0.0b3 -/// ├── c-2.0.0b4 -/// ├── c-2.0.0b5 -/// ├── c-2.0.0b6 -/// ├── c-2.0.0b7 -/// ├── c-2.0.0b8 -/// └── c-2.0.0b9 +/// └── c-2.0.0b1 /// ``` #[test] -fn transitive_prerelease_and_stable_dependency_many_versions_holes() { +fn transitive_prerelease_and_stable_dependency() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push(( - r"transitive-prerelease-and-stable-dependency-many-versions-holes-", - "package-", - )); + filters.push((r"transitive-prerelease-and-stable-dependency-", "package-")); uv_snapshot!(filters, command(&context) - .arg("transitive-prerelease-and-stable-dependency-many-versions-holes-a") - .arg("transitive-prerelease-and-stable-dependency-many-versions-holes-b") - , @r#" + .arg("transitive-prerelease-and-stable-dependency-a") + .arg("transitive-prerelease-and-stable-dependency-b") + , @r###" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because only the following versions of package-c are available: - package-c<1.0.0 - package-c>=2.0.0a5,<=2.0.0a7 - package-c==2.0.0b1 - package-c>=2.0.0b5 - and package-a==1.0.0 depends on one of: - package-c>1.0.0,<2.0.0a5 - package-c>2.0.0a7,<2.0.0b1 - package-c>2.0.0b1,<2.0.0b5 - we can conclude that package-a==1.0.0 cannot be used. + ╰─▶ Because there is no version of package-c==2.0.0b1 and package-a==1.0.0 depends on package-c==2.0.0b1, we can conclude that package-a==1.0.0 cannot be used. And because only package-a==1.0.0 is available and you require package-a, we can conclude that your requirements are unsatisfiable. - hint: package-c was requested with a pre-release marker (e.g., all of: - package-c>1.0.0,<2.0.0a5 - package-c>2.0.0a7,<2.0.0b1 - package-c>2.0.0b1,<2.0.0b5 - ), but pre-releases weren't enabled (try: `--prerelease=allow`) - "#); + hint: package-c was requested with a pre-release marker (e.g., package-c==2.0.0b1), but pre-releases weren't enabled (try: `--prerelease=allow`) + "###); // Since the user did not explicitly opt-in to a prerelease, it cannot be selected. assert_not_installed( &context.venv, - "transitive_prerelease_and_stable_dependency_many_versions_holes_a", + "transitive_prerelease_and_stable_dependency_a", &context.temp_dir, ); assert_not_installed( &context.venv, - "transitive_prerelease_and_stable_dependency_many_versions_holes_b", + "transitive_prerelease_and_stable_dependency_b", &context.temp_dir, ); } -/// The user requires a non-prerelease version of `a` which only has prerelease -/// versions available. There are pre-releases on the boundary of their range. +/// The user requires a package where recent versions require a Python version greater than the current version, but an older version is compatible. /// /// ```text -/// package-only-prereleases-boundary -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a<0.2.0 -/// │ └── unsatisfied: no matching version -/// └── a -/// ├── a-0.1.0a1 -/// ├── a-0.2.0a1 -/// └── a-0.3.0a1 -/// ``` -#[test] -fn package_only_prereleases_boundary() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"package-only-prereleases-boundary-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("package-only-prereleases-boundary-a<0.2.0") - , @r#" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Prepared 1 package in [TIME] - Installed 1 package in [TIME] - + package-a==0.1.0a1 - "#); - - // Since there are only prerelease versions of `a` available, a prerelease is - // allowed. Since the user did not explicitly request a pre-release, pre-releases - // at the boundary should not be selected. - assert_installed( - &context.venv, - "package_only_prereleases_boundary_a", - "0.1.0a1", - &context.temp_dir, - ); -} - -/// The user requires a non-prerelease version of `a` but has enabled pre-releases. -/// There are pre-releases on the boundary of their range. -/// -/// ```text -/// package-prereleases-boundary -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a<0.2.0 -/// │ └── satisfied by a-0.1.0 -/// └── a -/// ├── a-0.1.0 -/// ├── a-0.2.0a1 -/// └── a-0.3.0 -/// ``` -#[test] -fn package_prereleases_boundary() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"package-prereleases-boundary-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("--prerelease=allow") - .arg("package-prereleases-boundary-a<0.2.0") - , @r#" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Prepared 1 package in [TIME] - Installed 1 package in [TIME] - + package-a==0.1.0 - "#); - - // Since the user did not use a pre-release specifier, pre-releases at the boundary - // should not be selected even though pre-releases are allowed. - assert_installed( - &context.venv, - "package_prereleases_boundary_a", - "0.1.0", - &context.temp_dir, - ); -} - -/// The user requires a non-prerelease version of `a` but has enabled pre-releases. -/// There are pre-releases on the boundary of their range. -/// -/// ```text -/// package-prereleases-global-boundary -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a<0.2.0 -/// │ └── satisfied by a-0.1.0 -/// └── a -/// ├── a-0.1.0 -/// ├── a-0.2.0a1 -/// └── a-0.3.0 -/// ``` -#[test] -fn package_prereleases_global_boundary() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"package-prereleases-global-boundary-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("--prerelease=allow") - .arg("package-prereleases-global-boundary-a<0.2.0") - , @r#" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Prepared 1 package in [TIME] - Installed 1 package in [TIME] - + package-a==0.1.0 - "#); - - // Since the user did not use a pre-release specifier, pre-releases at the boundary - // should not be selected even though pre-releases are allowed. - assert_installed( - &context.venv, - "package_prereleases_global_boundary_a", - "0.1.0", - &context.temp_dir, - ); -} - -/// The user requires a prerelease version of `a`. There are pre-releases on the -/// boundary of their range. -/// -/// ```text -/// package-prereleases-specifier-boundary -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a<0.2.0a2 -/// │ └── satisfied by a-0.1.0 -/// └── a -/// ├── a-0.1.0 -/// ├── a-0.2.0 -/// ├── a-0.2.0a1 -/// ├── a-0.2.0a2 -/// ├── a-0.2.0a3 -/// └── a-0.3.0 -/// ``` -#[test] -fn package_prereleases_specifier_boundary() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"package-prereleases-specifier-boundary-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("package-prereleases-specifier-boundary-a<0.2.0a2") - , @r#" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Prepared 1 package in [TIME] - Installed 1 package in [TIME] - + package-a==0.2.0a1 - "#); - - // Since the user used a pre-release specifier, pre-releases at the boundary should - // be selected. - assert_installed( - &context.venv, - "package_prereleases_specifier_boundary_a", - "0.2.0a1", - &context.temp_dir, - ); -} - -/// The user requires a package which requires a Python version that does not exist -/// -/// ```text -/// python-version-does-not-exist -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a==1.0.0 -/// │ └── satisfied by a-1.0.0 -/// └── a -/// └── a-1.0.0 -/// └── requires python>=3.30 (incompatible with environment) -/// ``` -#[test] -fn python_version_does_not_exist() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"python-version-does-not-exist-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("python-version-does-not-exist-a==1.0.0") - , @r#" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.8.[X]) does not satisfy Python>=3.30 and package-a==1.0.0 depends on Python>=3.30, we can conclude that package-a==1.0.0 cannot be used. - And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. - "#); - - assert_not_installed( - &context.venv, - "python_version_does_not_exist_a", - &context.temp_dir, - ); -} - -/// The user requires a package which requires a Python version less than the -/// current version -/// -/// ```text -/// python-less-than-current +/// python-greater-than-current-backtrack /// ├── environment /// │ └── python3.9 /// ├── root -/// │ └── requires a==1.0.0 -/// │ └── satisfied by a-1.0.0 +/// │ └── requires a +/// │ ├── satisfied by a-1.0.0 +/// │ ├── satisfied by a-2.0.0 +/// │ ├── satisfied by a-3.0.0 +/// │ └── satisfied by a-4.0.0 /// └── a -/// └── a-1.0.0 -/// └── requires python<=3.8 (incompatible with environment) +/// ├── a-1.0.0 +/// ├── a-2.0.0 +/// │ └── requires python>=3.10 (incompatible with environment) +/// ├── a-3.0.0 +/// │ └── requires python>=3.11 (incompatible with environment) +/// └── a-4.0.0 +/// └── requires python>=3.12 (incompatible with environment) /// ``` #[test] -fn python_less_than_current() { +fn python_greater_than_current_backtrack() { let context = TestContext::new("3.9"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"python-less-than-current-", "package-")); + filters.push((r"python-greater-than-current-backtrack-", "package-")); uv_snapshot!(filters, command(&context) - .arg("python-less-than-current-a==1.0.0") - , @r#" + .arg("python-greater-than-current-backtrack-a") + , @r###" success: true exit_code: 0 ----- stdout ----- @@ -3752,98 +3637,81 @@ fn python_less_than_current() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.0.0 - "#); + "###); - // We ignore the upper bound on Python requirements + assert_installed( + &context.venv, + "python_greater_than_current_backtrack_a", + "1.0.0", + &context.temp_dir, + ); } -/// The user requires a package which requires a Python version greater than the -/// current version +/// The user requires a package where recent versions require a Python version greater than the current version, but an excluded older version is compatible. /// /// ```text -/// python-greater-than-current +/// python-greater-than-current-excluded /// ├── environment /// │ └── python3.9 /// ├── root -/// │ └── requires a==1.0.0 -/// │ └── satisfied by a-1.0.0 +/// │ └── requires a>=2.0.0 +/// │ ├── satisfied by a-2.0.0 +/// │ ├── satisfied by a-3.0.0 +/// │ └── satisfied by a-4.0.0 /// └── a -/// └── a-1.0.0 -/// └── requires python>=3.10 (incompatible with environment) +/// ├── a-1.0.0 +/// ├── a-2.0.0 +/// │ └── requires python>=3.10 (incompatible with environment) +/// ├── a-3.0.0 +/// │ └── requires python>=3.11 (incompatible with environment) +/// └── a-4.0.0 +/// └── requires python>=3.12 (incompatible with environment) /// ``` #[test] -fn python_greater_than_current() { +fn python_greater_than_current_excluded() { let context = TestContext::new("3.9"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"python-greater-than-current-", "package-")); + filters.push((r"python-greater-than-current-excluded-", "package-")); uv_snapshot!(filters, command(&context) - .arg("python-greater-than-current-a==1.0.0") - , @r#" + .arg("python-greater-than-current-excluded-a>=2.0.0") + , @r###" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.9.[X]) does not satisfy Python>=3.10 and package-a==1.0.0 depends on Python>=3.10, we can conclude that package-a==1.0.0 cannot be used. - And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. - "#); + ╰─▶ Because the current Python version (3.9.[X]) does not satisfy Python>=3.10,<3.11 and the current Python version (3.9.[X]) does not satisfy Python>=3.12, we can conclude that all of: + Python>=3.10,<3.11 + Python>=3.12 + are incompatible. + And because the current Python version (3.9.[X]) does not satisfy Python>=3.11,<3.12, we can conclude that Python>=3.10 is incompatible. + And because package-a==2.0.0 depends on Python>=3.10 and only the following versions of package-a are available: + package-a<=2.0.0 + package-a==3.0.0 + package-a==4.0.0 + we can conclude that package-a>=2.0.0,<3.0.0 cannot be used. (1) + + Because the current Python version (3.9.[X]) does not satisfy Python>=3.11,<3.12 and the current Python version (3.9.[X]) does not satisfy Python>=3.12, we can conclude that Python>=3.11 is incompatible. + And because package-a==3.0.0 depends on Python>=3.11, we can conclude that package-a==3.0.0 cannot be used. + And because we know from (1) that package-a>=2.0.0,<3.0.0 cannot be used, we can conclude that package-a>=2.0.0,<4.0.0 cannot be used. (2) + + Because the current Python version (3.9.[X]) does not satisfy Python>=3.12 and package-a==4.0.0 depends on Python>=3.12, we can conclude that package-a==4.0.0 cannot be used. + And because we know from (2) that package-a>=2.0.0,<4.0.0 cannot be used, we can conclude that package-a>=2.0.0 cannot be used. + And because you require package-a>=2.0.0, we can conclude that your requirements are unsatisfiable. + "###); assert_not_installed( &context.venv, - "python_greater_than_current_a", + "python_greater_than_current_excluded_a", &context.temp_dir, ); } -/// The user requires a package which requires a Python version with a patch version -/// greater than the current patch version -/// -/// ```text -/// python-greater-than-current-patch -/// ├── environment -/// │ └── python3.8.12 -/// ├── root -/// │ └── requires a==1.0.0 -/// │ └── satisfied by a-1.0.0 -/// └── a -/// └── a-1.0.0 -/// └── requires python>=3.8.14 (incompatible with environment) -/// ``` -#[cfg(feature = "python-patch")] -#[test] -fn python_greater_than_current_patch() { - let context = TestContext::new("3.8.12"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"python-greater-than-current-patch-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("python-greater-than-current-patch-a==1.0.0") - , @r#" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.8.12) does not satisfy Python>=3.8.14 and package-a==1.0.0 depends on Python>=3.8.14, we can conclude that package-a==1.0.0 cannot be used. - And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. - "#); - - assert_not_installed( - &context.venv, - "python_greater_than_current_patch_a", - &context.temp_dir, - ); -} - -/// The user requires a package which has many versions which all require a Python -/// version greater than the current version +/// The user requires a package which has many versions which all require a Python version greater than the current version /// /// ```text /// python-greater-than-current-many @@ -3888,7 +3756,7 @@ fn python_greater_than_current_many() { uv_snapshot!(filters, command(&context) .arg("python-greater-than-current-many-a==1.0.0") - , @r#" + , @r###" success: false exit_code: 1 ----- stdout ----- @@ -3896,7 +3764,7 @@ fn python_greater_than_current_many() { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because there is no version of package-a==1.0.0 and you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. - "#); + "###); assert_not_installed( &context.venv, @@ -3905,181 +3773,113 @@ fn python_greater_than_current_many() { ); } -/// The user requires a package where recent versions require a Python version -/// greater than the current version, but an older version is compatible. +/// The user requires a package which requires a Python version with a patch version greater than the current patch version /// /// ```text -/// python-greater-than-current-backtrack +/// python-greater-than-current-patch /// ├── environment -/// │ └── python3.9 +/// │ └── python3.8.12 /// ├── root -/// │ └── requires a -/// │ ├── satisfied by a-1.0.0 -/// │ ├── satisfied by a-2.0.0 -/// │ ├── satisfied by a-3.0.0 -/// │ └── satisfied by a-4.0.0 +/// │ └── requires a==1.0.0 +/// │ └── satisfied by a-1.0.0 /// └── a -/// ├── a-1.0.0 -/// ├── a-2.0.0 -/// │ └── requires python>=3.10 (incompatible with environment) -/// ├── a-3.0.0 -/// │ └── requires python>=3.11 (incompatible with environment) -/// └── a-4.0.0 -/// └── requires python>=3.12 (incompatible with environment) +/// └── a-1.0.0 +/// └── requires python>=3.8.14 (incompatible with environment) /// ``` +#[cfg(feature = "python-patch")] #[test] -fn python_greater_than_current_backtrack() { - let context = TestContext::new("3.9"); +fn python_greater_than_current_patch() { + let context = TestContext::new("3.8.12"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"python-greater-than-current-backtrack-", "package-")); + filters.push((r"python-greater-than-current-patch-", "package-")); uv_snapshot!(filters, command(&context) - .arg("python-greater-than-current-backtrack-a") - , @r#" - success: true - exit_code: 0 - ----- stdout ----- - - ----- stderr ----- - Resolved 1 package in [TIME] - Prepared 1 package in [TIME] - Installed 1 package in [TIME] - + package-a==1.0.0 - "#); - - assert_installed( - &context.venv, - "python_greater_than_current_backtrack_a", - "1.0.0", - &context.temp_dir, - ); -} - -/// The user requires a package where recent versions require a Python version -/// greater than the current version, but an excluded older version is compatible. -/// -/// ```text -/// python-greater-than-current-excluded -/// ├── environment -/// │ └── python3.9 -/// ├── root -/// │ └── requires a>=2.0.0 -/// │ ├── satisfied by a-2.0.0 -/// │ ├── satisfied by a-3.0.0 -/// │ └── satisfied by a-4.0.0 -/// └── a -/// ├── a-1.0.0 -/// ├── a-2.0.0 -/// │ └── requires python>=3.10 (incompatible with environment) -/// ├── a-3.0.0 -/// │ └── requires python>=3.11 (incompatible with environment) -/// └── a-4.0.0 -/// └── requires python>=3.12 (incompatible with environment) -/// ``` -#[test] -fn python_greater_than_current_excluded() { - let context = TestContext::new("3.9"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"python-greater-than-current-excluded-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("python-greater-than-current-excluded-a>=2.0.0") - , @r#" + .arg("python-greater-than-current-patch-a==1.0.0") + , @r###" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.9.[X]) does not satisfy Python>=3.10,<3.11 and the current Python version (3.9.[X]) does not satisfy Python>=3.12, we can conclude that all of: - Python>=3.10,<3.11 - Python>=3.12 - are incompatible. - And because the current Python version (3.9.[X]) does not satisfy Python>=3.11,<3.12, we can conclude that Python>=3.10 is incompatible. - And because package-a==2.0.0 depends on Python>=3.10 and only the following versions of package-a are available: - package-a<=2.0.0 - package-a==3.0.0 - package-a==4.0.0 - we can conclude that package-a>=2.0.0,<3.0.0 cannot be used. (1) - - Because the current Python version (3.9.[X]) does not satisfy Python>=3.11,<3.12 and the current Python version (3.9.[X]) does not satisfy Python>=3.12, we can conclude that Python>=3.11 is incompatible. - And because package-a==3.0.0 depends on Python>=3.11, we can conclude that package-a==3.0.0 cannot be used. - And because we know from (1) that package-a>=2.0.0,<3.0.0 cannot be used, we can conclude that package-a>=2.0.0,<4.0.0 cannot be used. (2) - - Because the current Python version (3.9.[X]) does not satisfy Python>=3.12 and package-a==4.0.0 depends on Python>=3.12, we can conclude that package-a==4.0.0 cannot be used. - And because we know from (2) that package-a>=2.0.0,<4.0.0 cannot be used, we can conclude that package-a>=2.0.0 cannot be used. - And because you require package-a>=2.0.0, we can conclude that your requirements are unsatisfiable. - "#); + ╰─▶ Because the current Python version (3.8.12) does not satisfy Python>=3.8.14 and package-a==1.0.0 depends on Python>=3.8.14, we can conclude that package-a==1.0.0 cannot be used. + And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. + "###); assert_not_installed( &context.venv, - "python_greater_than_current_excluded_a", + "python_greater_than_current_patch_a", &context.temp_dir, ); } -/// A wheel for a specific platform is available alongside the default. +/// The user requires a package which requires a Python version greater than the current version /// /// ```text -/// specific-tag-and-default +/// python-greater-than-current /// ├── environment -/// │ └── python3.8 +/// │ └── python3.9 /// ├── root -/// │ └── requires a +/// │ └── requires a==1.0.0 /// │ └── satisfied by a-1.0.0 /// └── a /// └── a-1.0.0 +/// └── requires python>=3.10 (incompatible with environment) /// ``` #[test] -fn specific_tag_and_default() { - let context = TestContext::new("3.8"); +fn python_greater_than_current() { + let context = TestContext::new("3.9"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"specific-tag-and-default-", "package-")); + filters.push((r"python-greater-than-current-", "package-")); uv_snapshot!(filters, command(&context) - .arg("specific-tag-and-default-a") - , @r#" - success: true - exit_code: 0 + .arg("python-greater-than-current-a==1.0.0") + , @r###" + success: false + exit_code: 1 ----- stdout ----- ----- stderr ----- - Resolved 1 package in [TIME] - Prepared 1 package in [TIME] - Installed 1 package in [TIME] - + package-a==1.0.0 - "#); + × No solution found when resolving dependencies: + ╰─▶ Because the current Python version (3.9.[X]) does not satisfy Python>=3.10 and package-a==1.0.0 depends on Python>=3.10, we can conclude that package-a==1.0.0 cannot be used. + And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. + "###); + + assert_not_installed( + &context.venv, + "python_greater_than_current_a", + &context.temp_dir, + ); } -/// No source distributions are available, only wheels. +/// The user requires a package which requires a Python version less than the current version /// /// ```text -/// only-wheels +/// python-less-than-current /// ├── environment -/// │ └── python3.8 +/// │ └── python3.9 /// ├── root -/// │ └── requires a +/// │ └── requires a==1.0.0 /// │ └── satisfied by a-1.0.0 /// └── a /// └── a-1.0.0 +/// └── requires python<=3.8 (incompatible with environment) /// ``` #[test] -fn only_wheels() { - let context = TestContext::new("3.8"); +fn python_less_than_current() { + let context = TestContext::new("3.9"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"only-wheels-", "package-")); + filters.push((r"python-less-than-current-", "package-")); uv_snapshot!(filters, command(&context) - .arg("only-wheels-a") - , @r#" + .arg("python-less-than-current-a==1.0.0") + , @r###" success: true exit_code: 0 ----- stdout ----- @@ -4089,13 +3889,56 @@ fn only_wheels() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.0.0 - "#); + "###); + + // We ignore the upper bound on Python requirements } -/// No wheels are available, only source distributions. +/// The user requires a package which requires a Python version that does not exist /// /// ```text -/// no-wheels +/// python-version-does-not-exist +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a==1.0.0 +/// │ └── satisfied by a-1.0.0 +/// └── a +/// └── a-1.0.0 +/// └── requires python>=3.30 (incompatible with environment) +/// ``` +#[test] +fn python_version_does_not_exist() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"python-version-does-not-exist-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("python-version-does-not-exist-a==1.0.0") + , @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because the current Python version (3.8.[X]) does not satisfy Python>=3.30 and package-a==1.0.0 depends on Python>=3.30, we can conclude that package-a==1.0.0 cannot be used. + And because you require package-a==1.0.0, we can conclude that your requirements are unsatisfiable. + "###); + + assert_not_installed( + &context.venv, + "python_version_does_not_exist_a", + &context.temp_dir, + ); +} + +/// Both wheels and source distributions are available, and the user has disabled binaries. +/// +/// ```text +/// no-binary /// ├── environment /// │ └── python3.8 /// ├── root @@ -4105,16 +3948,18 @@ fn only_wheels() { /// └── a-1.0.0 /// ``` #[test] -fn no_wheels() { +fn no_binary() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"no-wheels-", "package-")); + filters.push((r"no-binary-", "package-")); uv_snapshot!(filters, command(&context) - .arg("no-wheels-a") - , @r#" + .arg("--no-binary") + .arg("no-binary-a") + .arg("no-binary-a") + , @r###" success: true exit_code: 0 ----- stdout ----- @@ -4124,7 +3969,206 @@ fn no_wheels() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.0.0 - "#); + "###); + + // The source distribution should be used for install +} + +/// Both wheels and source distributions are available, and the user has disabled builds. +/// +/// ```text +/// no-build +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a +/// │ └── satisfied by a-1.0.0 +/// └── a +/// └── a-1.0.0 +/// ``` +#[test] +fn no_build() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"no-build-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("--only-binary") + .arg("no-build-a") + .arg("no-build-a") + , @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + package-a==1.0.0 + "###); + + // The wheel should be used for install +} + +/// No wheels with matching ABI tags are available, nor are any source distributions available +/// +/// ```text +/// no-sdist-no-wheels-with-matching-abi +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a +/// │ └── satisfied by a-1.0.0 +/// └── a +/// └── a-1.0.0 +/// ``` +#[test] +fn no_sdist_no_wheels_with_matching_abi() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"no-sdist-no-wheels-with-matching-abi-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("no-sdist-no-wheels-with-matching-abi-a") + , @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because only package-a==1.0.0 is available and package-a==1.0.0 has no wheels with a matching Python ABI tag, we can conclude that all versions of package-a cannot be used. + And because you require package-a, we can conclude that your requirements are unsatisfiable. + "###); + + assert_not_installed( + &context.venv, + "no_sdist_no_wheels_with_matching_abi_a", + &context.temp_dir, + ); +} + +/// No wheels with matching platform tags are available, nor are any source distributions available +/// +/// ```text +/// no-sdist-no-wheels-with-matching-platform +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a +/// │ └── satisfied by a-1.0.0 +/// └── a +/// └── a-1.0.0 +/// ``` +#[test] +fn no_sdist_no_wheels_with_matching_platform() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"no-sdist-no-wheels-with-matching-platform-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("no-sdist-no-wheels-with-matching-platform-a") + , @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because only package-a==1.0.0 is available and package-a==1.0.0 has no wheels with a matching platform tag, we can conclude that all versions of package-a cannot be used. + And because you require package-a, we can conclude that your requirements are unsatisfiable. + "###); + + assert_not_installed( + &context.venv, + "no_sdist_no_wheels_with_matching_platform_a", + &context.temp_dir, + ); +} + +/// No wheels with matching Python tags are available, nor are any source distributions available +/// +/// ```text +/// no-sdist-no-wheels-with-matching-python +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a +/// │ └── satisfied by a-1.0.0 +/// └── a +/// └── a-1.0.0 +/// ``` +#[test] +fn no_sdist_no_wheels_with_matching_python() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"no-sdist-no-wheels-with-matching-python-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("no-sdist-no-wheels-with-matching-python-a") + , @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because only package-a==1.0.0 is available and package-a==1.0.0 has no wheels with a matching Python implementation tag, we can conclude that all versions of package-a cannot be used. + And because you require package-a, we can conclude that your requirements are unsatisfiable. + "###); + + assert_not_installed( + &context.venv, + "no_sdist_no_wheels_with_matching_python_a", + &context.temp_dir, + ); +} + +/// No wheels are available, only source distributions but the user has disabled builds. +/// +/// ```text +/// no-wheels-no-build +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a +/// │ └── satisfied by a-1.0.0 +/// └── a +/// └── a-1.0.0 +/// ``` +#[test] +fn no_wheels_no_build() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"no-wheels-no-build-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("--only-binary") + .arg("no-wheels-no-build-a") + .arg("no-wheels-no-build-a") + , @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because only package-a==1.0.0 is available and package-a==1.0.0 has no usable wheels and building from source is disabled, we can conclude that all versions of package-a cannot be used. + And because you require package-a, we can conclude that your requirements are unsatisfiable. + "###); + + assert_not_installed(&context.venv, "no_wheels_no_build_a", &context.temp_dir); } /// No wheels with matching platform tags are available, just source distributions. @@ -4149,7 +4193,7 @@ fn no_wheels_with_matching_platform() { uv_snapshot!(filters, command(&context) .arg("no-wheels-with-matching-platform-a") - , @r#" + , @r###" success: true exit_code: 0 ----- stdout ----- @@ -4159,14 +4203,13 @@ fn no_wheels_with_matching_platform() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.0.0 - "#); + "###); } -/// No wheels with matching platform tags are available, nor are any source -/// distributions available +/// No wheels are available, only source distributions. /// /// ```text -/// no-sdist-no-wheels-with-matching-platform +/// no-wheels /// ├── environment /// │ └── python3.8 /// ├── root @@ -4176,156 +4219,29 @@ fn no_wheels_with_matching_platform() { /// └── a-1.0.0 /// ``` #[test] -fn no_sdist_no_wheels_with_matching_platform() { +fn no_wheels() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"no-sdist-no-wheels-with-matching-platform-", "package-")); + filters.push((r"no-wheels-", "package-")); uv_snapshot!(filters, command(&context) - .arg("no-sdist-no-wheels-with-matching-platform-a") - , @r#" - success: false - exit_code: 1 + .arg("no-wheels-a") + , @r###" + success: true + exit_code: 0 ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only package-a==1.0.0 is available and package-a==1.0.0 has no wheels with a matching platform tag, we can conclude that all versions of package-a cannot be used. - And because you require package-a, we can conclude that your requirements are unsatisfiable. - "#); - - assert_not_installed( - &context.venv, - "no_sdist_no_wheels_with_matching_platform_a", - &context.temp_dir, - ); + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + package-a==1.0.0 + "###); } -/// No wheels with matching Python tags are available, nor are any source -/// distributions available -/// -/// ```text -/// no-sdist-no-wheels-with-matching-python -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a -/// │ └── satisfied by a-1.0.0 -/// └── a -/// └── a-1.0.0 -/// ``` -#[test] -fn no_sdist_no_wheels_with_matching_python() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"no-sdist-no-wheels-with-matching-python-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("no-sdist-no-wheels-with-matching-python-a") - , @r#" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only package-a==1.0.0 is available and package-a==1.0.0 has no wheels with a matching Python implementation tag, we can conclude that all versions of package-a cannot be used. - And because you require package-a, we can conclude that your requirements are unsatisfiable. - "#); - - assert_not_installed( - &context.venv, - "no_sdist_no_wheels_with_matching_python_a", - &context.temp_dir, - ); -} - -/// No wheels with matching ABI tags are available, nor are any source distributions -/// available -/// -/// ```text -/// no-sdist-no-wheels-with-matching-abi -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a -/// │ └── satisfied by a-1.0.0 -/// └── a -/// └── a-1.0.0 -/// ``` -#[test] -fn no_sdist_no_wheels_with_matching_abi() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"no-sdist-no-wheels-with-matching-abi-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("no-sdist-no-wheels-with-matching-abi-a") - , @r#" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only package-a==1.0.0 is available and package-a==1.0.0 has no wheels with a matching Python ABI tag, we can conclude that all versions of package-a cannot be used. - And because you require package-a, we can conclude that your requirements are unsatisfiable. - "#); - - assert_not_installed( - &context.venv, - "no_sdist_no_wheels_with_matching_abi_a", - &context.temp_dir, - ); -} - -/// No wheels are available, only source distributions but the user has disabled -/// builds. -/// -/// ```text -/// no-wheels-no-build -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a -/// │ └── satisfied by a-1.0.0 -/// └── a -/// └── a-1.0.0 -/// ``` -#[test] -fn no_wheels_no_build() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"no-wheels-no-build-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("--only-binary") - .arg("no-wheels-no-build-a") - .arg("no-wheels-no-build-a") - , @r#" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only package-a==1.0.0 is available and package-a==1.0.0 has no usable wheels and building from source is disabled, we can conclude that all versions of package-a cannot be used. - And because you require package-a, we can conclude that your requirements are unsatisfiable. - "#); - - assert_not_installed(&context.venv, "no_wheels_no_build_a", &context.temp_dir); -} - -/// No source distributions are available, only wheels but the user has disabled -/// using pre-built binaries. +/// No source distributions are available, only wheels but the user has disabled using pre-built binaries. /// /// ```text /// only-wheels-no-binary @@ -4349,7 +4265,7 @@ fn only_wheels_no_binary() { .arg("--no-binary") .arg("only-wheels-no-binary-a") .arg("only-wheels-no-binary-a") - , @r#" + , @r###" success: false exit_code: 1 ----- stdout ----- @@ -4358,16 +4274,15 @@ fn only_wheels_no_binary() { × No solution found when resolving dependencies: ╰─▶ Because only package-a==1.0.0 is available and package-a==1.0.0 has no source distribution and using wheels is disabled, we can conclude that all versions of package-a cannot be used. And because you require package-a, we can conclude that your requirements are unsatisfiable. - "#); + "###); assert_not_installed(&context.venv, "only_wheels_no_binary_a", &context.temp_dir); } -/// Both wheels and source distributions are available, and the user has disabled -/// builds. +/// No source distributions are available, only wheels. /// /// ```text -/// no-build +/// only-wheels /// ├── environment /// │ └── python3.8 /// ├── root @@ -4377,18 +4292,16 @@ fn only_wheels_no_binary() { /// └── a-1.0.0 /// ``` #[test] -fn no_build() { +fn only_wheels() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"no-build-", "package-")); + filters.push((r"only-wheels-", "package-")); uv_snapshot!(filters, command(&context) - .arg("--only-binary") - .arg("no-build-a") - .arg("no-build-a") - , @r#" + .arg("only-wheels-a") + , @r###" success: true exit_code: 0 ----- stdout ----- @@ -4398,16 +4311,13 @@ fn no_build() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.0.0 - "#); - - // The wheel should be used for install + "###); } -/// Both wheels and source distributions are available, and the user has disabled -/// binaries. +/// A wheel for a specific platform is available alongside the default. /// /// ```text -/// no-binary +/// specific-tag-and-default /// ├── environment /// │ └── python3.8 /// ├── root @@ -4417,18 +4327,16 @@ fn no_build() { /// └── a-1.0.0 /// ``` #[test] -fn no_binary() { +fn specific_tag_and_default() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"no-binary-", "package-")); + filters.push((r"specific-tag-and-default-", "package-")); uv_snapshot!(filters, command(&context) - .arg("--no-binary") - .arg("no-binary-a") - .arg("no-binary-a") - , @r#" + .arg("specific-tag-and-default-a") + , @r###" success: true exit_code: 0 ----- stdout ----- @@ -4438,48 +4346,7 @@ fn no_binary() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.0.0 - "#); - - // The source distribution should be used for install -} - -/// The user requires any version of package `a` which only has yanked versions -/// available. -/// -/// ```text -/// package-only-yanked -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a -/// │ └── unsatisfied: no matching version -/// └── a -/// └── a-1.0.0 (yanked) -/// ``` -#[test] -fn package_only_yanked() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"package-only-yanked-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("package-only-yanked-a") - , @r#" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only package-a==1.0.0 is available and package-a==1.0.0 was yanked (reason: Yanked for testing), we can conclude that all versions of package-a cannot be used. - And because you require package-a, we can conclude that your requirements are unsatisfiable. - "#); - - // Yanked versions should not be installed, even if they are the only one - // available. - assert_not_installed(&context.venv, "package_only_yanked_a", &context.temp_dir); + "###); } /// The user requires a version of package `a` which only matches yanked versions. @@ -4505,7 +4372,7 @@ fn package_only_yanked_in_range() { uv_snapshot!(filters, command(&context) .arg("package-only-yanked-in-range-a>0.1.0") - , @r#" + , @r###" success: false exit_code: 1 ----- stdout ----- @@ -4517,10 +4384,9 @@ fn package_only_yanked_in_range() { package-a==1.0.0 and package-a==1.0.0 was yanked (reason: Yanked for testing), we can conclude that package-a>0.1.0 cannot be used. And because you require package-a>0.1.0, we can conclude that your requirements are unsatisfiable. - "#); + "###); - // Since there are other versions of `a` available, yanked versions should not be - // selected without explicit opt-in. + // Since there are other versions of `a` available, yanked versions should not be selected without explicit opt-in. assert_not_installed( &context.venv, "package_only_yanked_in_range_a", @@ -4528,53 +4394,44 @@ fn package_only_yanked_in_range() { ); } -/// The user requires any version of package `a` has a yanked version available and -/// an older unyanked version. +/// The user requires any version of package `a` which only has yanked versions available. /// /// ```text -/// requires-package-yanked-and-unyanked-any +/// package-only-yanked /// ├── environment /// │ └── python3.8 /// ├── root /// │ └── requires a -/// │ └── satisfied by a-0.1.0 +/// │ └── unsatisfied: no matching version /// └── a -/// ├── a-0.1.0 /// └── a-1.0.0 (yanked) /// ``` #[test] -fn requires_package_yanked_and_unyanked_any() { +fn package_only_yanked() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"requires-package-yanked-and-unyanked-any-", "package-")); + filters.push((r"package-only-yanked-", "package-")); uv_snapshot!(filters, command(&context) - .arg("requires-package-yanked-and-unyanked-any-a") - , @r#" - success: true - exit_code: 0 + .arg("package-only-yanked-a") + , @r###" + success: false + exit_code: 1 ----- stdout ----- ----- stderr ----- - Resolved 1 package in [TIME] - Prepared 1 package in [TIME] - Installed 1 package in [TIME] - + package-a==0.1.0 - "#); + × No solution found when resolving dependencies: + ╰─▶ Because only package-a==1.0.0 is available and package-a==1.0.0 was yanked (reason: Yanked for testing), we can conclude that all versions of package-a cannot be used. + And because you require package-a, we can conclude that your requirements are unsatisfiable. + "###); - // The unyanked version should be selected. - assert_installed( - &context.venv, - "requires_package_yanked_and_unyanked_any_a", - "0.1.0", - &context.temp_dir, - ); + // Yanked versions should not be installed, even if they are the only one available. + assert_not_installed(&context.venv, "package_only_yanked_a", &context.temp_dir); } -/// The user requires any version of `a` and both yanked and unyanked releases are -/// available. +/// The user requires any version of `a` and both yanked and unyanked releases are available. /// /// ```text /// package-yanked-specified-mixed-available @@ -4600,7 +4457,7 @@ fn package_yanked_specified_mixed_available() { uv_snapshot!(filters, command(&context) .arg("package-yanked-specified-mixed-available-a>=0.1.0") - , @r#" + , @r###" success: true exit_code: 0 ----- stdout ----- @@ -4610,7 +4467,7 @@ fn package_yanked_specified_mixed_available() { Prepared 1 package in [TIME] Installed 1 package in [TIME] + package-a==0.3.0 - "#); + "###); // The latest unyanked version should be selected. assert_installed( @@ -4621,109 +4478,51 @@ fn package_yanked_specified_mixed_available() { ); } -/// The user requires any version of package `a` which requires `b` which only has -/// yanked versions available. +/// The user requires any version of package `a` has a yanked version available and an older unyanked version. /// /// ```text -/// transitive-package-only-yanked +/// requires-package-yanked-and-unyanked-any /// ├── environment /// │ └── python3.8 /// ├── root /// │ └── requires a /// │ └── satisfied by a-0.1.0 -/// ├── a -/// │ └── a-0.1.0 -/// │ └── requires b -/// │ └── unsatisfied: no matching version -/// └── b -/// └── b-1.0.0 (yanked) +/// └── a +/// ├── a-0.1.0 +/// └── a-1.0.0 (yanked) /// ``` #[test] -fn transitive_package_only_yanked() { +fn requires_package_yanked_and_unyanked_any() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"transitive-package-only-yanked-", "package-")); + filters.push((r"requires-package-yanked-and-unyanked-any-", "package-")); uv_snapshot!(filters, command(&context) - .arg("transitive-package-only-yanked-a") - , @r#" - success: false - exit_code: 1 + .arg("requires-package-yanked-and-unyanked-any-a") + , @r###" + success: true + exit_code: 0 ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only package-b==1.0.0 is available and package-b==1.0.0 was yanked (reason: Yanked for testing), we can conclude that all versions of package-b cannot be used. - And because package-a==0.1.0 depends on package-b, we can conclude that package-a==0.1.0 cannot be used. - And because only package-a==0.1.0 is available and you require package-a, we can conclude that your requirements are unsatisfiable. - "#); + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + package-a==0.1.0 + "###); - // Yanked versions should not be installed, even if they are the only one - // available. - assert_not_installed( + // The unyanked version should be selected. + assert_installed( &context.venv, - "transitive_package_only_yanked_a", + "requires_package_yanked_and_unyanked_any_a", + "0.1.0", &context.temp_dir, ); } -/// The user requires package `a` which has a dependency on a package which only -/// matches yanked versions. -/// -/// ```text -/// transitive-package-only-yanked-in-range -/// ├── environment -/// │ └── python3.8 -/// ├── root -/// │ └── requires a -/// │ └── satisfied by a-0.1.0 -/// ├── a -/// │ └── a-0.1.0 -/// │ └── requires b>0.1 -/// │ └── unsatisfied: no matching version -/// └── b -/// ├── b-0.1.0 -/// └── b-1.0.0 (yanked) -/// ``` -#[test] -fn transitive_package_only_yanked_in_range() { - let context = TestContext::new("3.8"); - - // In addition to the standard filters, swap out package names for shorter messages - let mut filters = context.filters(); - filters.push((r"transitive-package-only-yanked-in-range-", "package-")); - - uv_snapshot!(filters, command(&context) - .arg("transitive-package-only-yanked-in-range-a") - , @r#" - success: false - exit_code: 1 - ----- stdout ----- - - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because only the following versions of package-b are available: - package-b<0.1 - package-b==1.0.0 - and package-b==1.0.0 was yanked (reason: Yanked for testing), we can conclude that package-b>0.1 cannot be used. - And because package-a==0.1.0 depends on package-b>0.1, we can conclude that package-a==0.1.0 cannot be used. - And because only package-a==0.1.0 is available and you require package-a, we can conclude that your requirements are unsatisfiable. - "#); - - // Yanked versions should not be installed, even if they are the only valid version - // in a range. - assert_not_installed( - &context.venv, - "transitive_package_only_yanked_in_range_a", - &context.temp_dir, - ); -} - -/// The user requires package `a` which has a dependency on a package which only -/// matches yanked versions; the user has opted into allowing the yanked version of -/// `b` explicitly. +/// The user requires package `a` which has a dependency on a package which only matches yanked versions; the user has opted into allowing the yanked version of `b` explicitly. /// /// ```text /// transitive-package-only-yanked-in-range-opt-in @@ -4756,7 +4555,7 @@ fn transitive_package_only_yanked_in_range_opt_in() { uv_snapshot!(filters, command(&context) .arg("transitive-package-only-yanked-in-range-opt-in-a") .arg("transitive-package-only-yanked-in-range-opt-in-b==1.0.0") - , @r#" + , @r###" success: true exit_code: 0 ----- stdout ----- @@ -4768,10 +4567,9 @@ fn transitive_package_only_yanked_in_range_opt_in() { + package-a==0.1.0 + package-b==1.0.0 warning: `package-b==1.0.0` is yanked (reason: "Yanked for testing") - "#); + "###); - // Since the user included a dependency on `b` with an exact specifier, the yanked - // version can be selected. + // Since the user included a dependency on `b` with an exact specifier, the yanked version can be selected. assert_installed( &context.venv, "transitive_package_only_yanked_in_range_opt_in_a", @@ -4786,68 +4584,103 @@ fn transitive_package_only_yanked_in_range_opt_in() { ); } -/// A transitive dependency has both a yanked and an unyanked version, but can only -/// be satisfied by a yanked version +/// The user requires package `a` which has a dependency on a package which only matches yanked versions. /// /// ```text -/// transitive-yanked-and-unyanked-dependency +/// transitive-package-only-yanked-in-range /// ├── environment /// │ └── python3.8 /// ├── root -/// │ ├── requires a -/// │ │ └── satisfied by a-1.0.0 -/// │ └── requires b -/// │ └── satisfied by b-1.0.0 +/// │ └── requires a +/// │ └── satisfied by a-0.1.0 /// ├── a -/// │ └── a-1.0.0 -/// │ └── requires c==2.0.0 +/// │ └── a-0.1.0 +/// │ └── requires b>0.1 /// │ └── unsatisfied: no matching version -/// ├── b -/// │ └── b-1.0.0 -/// │ └── requires c<=3.0.0,>=1.0.0 -/// │ └── satisfied by c-1.0.0 -/// └── c -/// ├── c-1.0.0 -/// └── c-2.0.0 (yanked) +/// └── b +/// ├── b-0.1.0 +/// └── b-1.0.0 (yanked) /// ``` #[test] -fn transitive_yanked_and_unyanked_dependency() { +fn transitive_package_only_yanked_in_range() { let context = TestContext::new("3.8"); // In addition to the standard filters, swap out package names for shorter messages let mut filters = context.filters(); - filters.push((r"transitive-yanked-and-unyanked-dependency-", "package-")); + filters.push((r"transitive-package-only-yanked-in-range-", "package-")); uv_snapshot!(filters, command(&context) - .arg("transitive-yanked-and-unyanked-dependency-a") - .arg("transitive-yanked-and-unyanked-dependency-b") - , @r#" + .arg("transitive-package-only-yanked-in-range-a") + , @r###" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because package-c==2.0.0 was yanked (reason: Yanked for testing) and package-a==1.0.0 depends on package-c==2.0.0, we can conclude that package-a==1.0.0 cannot be used. - And because only package-a==1.0.0 is available and you require package-a, we can conclude that your requirements are unsatisfiable. - "#); + ╰─▶ Because only the following versions of package-b are available: + package-b<0.1 + package-b==1.0.0 + and package-b==1.0.0 was yanked (reason: Yanked for testing), we can conclude that package-b>0.1 cannot be used. + And because package-a==0.1.0 depends on package-b>0.1, we can conclude that package-a==0.1.0 cannot be used. + And because only package-a==0.1.0 is available and you require package-a, we can conclude that your requirements are unsatisfiable. + "###); - // Since the user did not explicitly select the yanked version, it cannot be used. + // Yanked versions should not be installed, even if they are the only valid version in a range. assert_not_installed( &context.venv, - "transitive_yanked_and_unyanked_dependency_a", - &context.temp_dir, - ); - assert_not_installed( - &context.venv, - "transitive_yanked_and_unyanked_dependency_b", + "transitive_package_only_yanked_in_range_a", &context.temp_dir, ); } -/// A transitive dependency has both a yanked and an unyanked version, but can only -/// be satisfied by a yanked. The user includes an opt-in to the yanked version of -/// the transitive dependency. +/// The user requires any version of package `a` which requires `b` which only has yanked versions available. +/// +/// ```text +/// transitive-package-only-yanked +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ └── requires a +/// │ └── satisfied by a-0.1.0 +/// ├── a +/// │ └── a-0.1.0 +/// │ └── requires b +/// │ └── unsatisfied: no matching version +/// └── b +/// └── b-1.0.0 (yanked) +/// ``` +#[test] +fn transitive_package_only_yanked() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"transitive-package-only-yanked-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("transitive-package-only-yanked-a") + , @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because only package-b==1.0.0 is available and package-b==1.0.0 was yanked (reason: Yanked for testing), we can conclude that all versions of package-b cannot be used. + And because package-a==0.1.0 depends on package-b, we can conclude that package-a==0.1.0 cannot be used. + And because only package-a==0.1.0 is available and you require package-a, we can conclude that your requirements are unsatisfiable. + "###); + + // Yanked versions should not be installed, even if they are the only one available. + assert_not_installed( + &context.venv, + "transitive_package_only_yanked_a", + &context.temp_dir, + ); +} + +/// A transitive dependency has both a yanked and an unyanked version, but can only be satisfied by a yanked. The user includes an opt-in to the yanked version of the transitive dependency. /// /// ```text /// transitive-yanked-and-unyanked-dependency-opt-in @@ -4887,7 +4720,7 @@ fn transitive_yanked_and_unyanked_dependency_opt_in() { .arg("transitive-yanked-and-unyanked-dependency-opt-in-a") .arg("transitive-yanked-and-unyanked-dependency-opt-in-b") .arg("transitive-yanked-and-unyanked-dependency-opt-in-c==2.0.0") - , @r#" + , @r###" success: true exit_code: 0 ----- stdout ----- @@ -4900,10 +4733,9 @@ fn transitive_yanked_and_unyanked_dependency_opt_in() { + package-b==1.0.0 + package-c==2.0.0 warning: `package-c==2.0.0` is yanked (reason: "Yanked for testing") - "#); + "###); - // Since the user explicitly selected the yanked version of `c`, it can be - // installed. + // Since the user explicitly selected the yanked version of `c`, it can be installed. assert_installed( &context.venv, "transitive_yanked_and_unyanked_dependency_opt_in_a", @@ -4923,3 +4755,61 @@ fn transitive_yanked_and_unyanked_dependency_opt_in() { &context.temp_dir, ); } + +/// A transitive dependency has both a yanked and an unyanked version, but can only be satisfied by a yanked version +/// +/// ```text +/// transitive-yanked-and-unyanked-dependency +/// ├── environment +/// │ └── python3.8 +/// ├── root +/// │ ├── requires a +/// │ │ └── satisfied by a-1.0.0 +/// │ └── requires b +/// │ └── satisfied by b-1.0.0 +/// ├── a +/// │ └── a-1.0.0 +/// │ └── requires c==2.0.0 +/// │ └── unsatisfied: no matching version +/// ├── b +/// │ └── b-1.0.0 +/// │ └── requires c<=3.0.0,>=1.0.0 +/// │ └── satisfied by c-1.0.0 +/// └── c +/// ├── c-1.0.0 +/// └── c-2.0.0 (yanked) +/// ``` +#[test] +fn transitive_yanked_and_unyanked_dependency() { + let context = TestContext::new("3.8"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"transitive-yanked-and-unyanked-dependency-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("transitive-yanked-and-unyanked-dependency-a") + .arg("transitive-yanked-and-unyanked-dependency-b") + , @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because package-c==2.0.0 was yanked (reason: Yanked for testing) and package-a==1.0.0 depends on package-c==2.0.0, we can conclude that package-a==1.0.0 cannot be used. + And because only package-a==1.0.0 is available and you require package-a, we can conclude that your requirements are unsatisfiable. + "###); + + // Since the user did not explicitly select the yanked version, it cannot be used. + assert_not_installed( + &context.venv, + "transitive_yanked_and_unyanked_dependency_a", + &context.temp_dir, + ); + assert_not_installed( + &context.venv, + "transitive_yanked_and_unyanked_dependency_b", + &context.temp_dir, + ); +} diff --git a/scripts/scenarios/generate.py b/scripts/scenarios/generate.py index 3b5759028..af1db5ff4 100755 --- a/scripts/scenarios/generate.py +++ b/scripts/scenarios/generate.py @@ -172,7 +172,6 @@ def main(scenarios: list[Path], snapshot_update: bool = True): for scenario in data["scenarios"]: resolver_options = scenario["resolver_options"] or {} if resolver_options.get("universal"): - print(scenario["name"]) lock_scenarios.append(scenario) elif resolver_options.get("python") is not None: compile_scenarios.append(scenario) diff --git a/scripts/scenarios/requirements.in b/scripts/scenarios/requirements.in index 8ee979494..fd904327a 100644 --- a/scripts/scenarios/requirements.in +++ b/scripts/scenarios/requirements.in @@ -1,2 +1,2 @@ chevron-blue -packse>=0.3.37 +packse>=0.3.39 diff --git a/scripts/scenarios/requirements.txt b/scripts/scenarios/requirements.txt index f139b2786..e53e4bf90 100644 --- a/scripts/scenarios/requirements.txt +++ b/scripts/scenarios/requirements.txt @@ -1,90 +1,26 @@ # This file was autogenerated by uv via the following command: # uv pip compile scripts/scenarios/requirements.in -o scripts/scenarios/requirements.txt --refresh-package packse -n -certifi==2024.2.2 - # via requests -cffi==1.16.0 - # via cryptography -charset-normalizer==3.3.2 - # via requests chevron-blue==0.2.1 # via # -r scripts/scenarios/requirements.in # packse -cryptography==42.0.7 - # via secretstorage -docutils==0.21.2 - # via readme-renderer hatchling==1.24.2 # via packse -idna==3.7 - # via requests -importlib-metadata==7.1.0 - # via twine -jaraco-classes==3.4.0 - # via keyring -jaraco-context==5.3.0 - # via keyring -jaraco-functools==4.0.1 - # via keyring -jeepney==0.8.0 - # via - # keyring - # secretstorage -keyring==25.1.0 - # via twine -markdown-it-py==3.0.0 - # via rich -mdurl==0.1.2 - # via markdown-it-py -more-itertools==10.2.0 - # via - # jaraco-classes - # jaraco-functools msgspec==0.18.6 # via packse -nh3==0.2.17 - # via readme-renderer packaging==24.0 # via hatchling -packse==0.3.37 +packse==0.3.39 # via -r scripts/scenarios/requirements.in pathspec==0.12.1 # via hatchling -pkginfo==1.10.0 - # via twine pluggy==1.5.0 # via hatchling -pycparser==2.22 - # via cffi -pygments==2.17.2 - # via - # readme-renderer - # rich pyyaml==6.0.1 # via packse -readme-renderer==43.0 - # via twine -requests==2.31.0 - # via - # requests-toolbelt - # twine -requests-toolbelt==1.0.0 - # via twine -rfc3986==2.0.0 - # via twine -rich==13.7.1 - # via twine -secretstorage==3.3.3 - # via keyring setuptools==69.5.1 # via packse trove-classifiers==2024.4.10 # via hatchling -twine==4.0.2 +uv==0.4.29 # via packse -urllib3==2.2.1 - # via - # requests - # twine -zipp==3.18.1 - # via importlib-metadata