From 585dac12a9a1f8167042c5c29e5b70d6e2d51350 Mon Sep 17 00:00:00 2001 From: konsti Date: Wed, 18 Feb 2026 19:35:30 +0100 Subject: [PATCH] Update packse and remove Python 3.9.20 from test requirements (#17881) Requires the packse PR to land first. --- .python-versions | 2 - crates/uv-test/src/lib.rs | 2 +- crates/uv/tests/it/lock_scenarios.rs | 102 +++++++++++++++- crates/uv/tests/it/pip_compile_scenarios.rs | 55 ++++----- crates/uv/tests/it/pip_install_scenarios.rs | 117 ++++++++++++++++++- scripts/scenarios/generate.py | 6 +- scripts/scenarios/pylock.toml | 6 +- scripts/scenarios/pyproject.toml | 2 +- scripts/scenarios/templates/compile.mustache | 2 +- scripts/sync-python-version-constants.py | 4 +- 10 files changed, 255 insertions(+), 43 deletions(-) diff --git a/.python-versions b/.python-versions index 0efa312382..9aedcb77aa 100644 --- a/.python-versions +++ b/.python-versions @@ -15,8 +15,6 @@ 3.10.16 3.9.21 3.8.20 -# The following are required for packse scenarios -3.9.20 # The following is needed for `==3.13` request tests 3.13.0 # A pre-release version required for testing diff --git a/crates/uv-test/src/lib.rs b/crates/uv-test/src/lib.rs index 46f55c60d3..c0ccab398e 100755 --- a/crates/uv-test/src/lib.rs +++ b/crates/uv-test/src/lib.rs @@ -36,7 +36,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.53"; +pub const PACKSE_VERSION: &str = "0.3.59"; pub const DEFAULT_PYTHON_VERSION: &str = "3.12"; // The expected latest patch version for each Python minor version. diff --git a/crates/uv/tests/it/lock_scenarios.rs b/crates/uv/tests/it/lock_scenarios.rs index 6a413874cc..8848870f09 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 = "test-python", feature = "test-pypi"))] #![allow(clippy::needless_raw_string_hashes)] @@ -5347,6 +5347,106 @@ fn virtual_package_extra_priorities() -> Result<()> { Ok(()) } +/// While both Linux and Windows are required and `win-only` has only a Windows wheel, `win-only` is also used only on Windows. +/// +/// ```text +/// requires-python-subset +/// ├── environment +/// │ └── python3.12 +/// ├── root +/// │ └── requires win-only; sys_platform == "win32" +/// │ └── satisfied by win-only-1.0.0 +/// └── win-only +/// └── win-only-1.0.0 +/// ``` +#[test] +fn requires_python_subset() -> Result<()> { + let context = uv_test::test_context!("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-subset-", "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-subset-win-only; sys_platform == "win32"''', + ] + requires-python = ">=3.12" + [tool.uv] + required-environments = [ + '''sys_platform == "linux"''', + '''sys_platform == "win32"''', + ] + "###, + )?; + + let mut cmd = context.lock(); + cmd.env_remove(EnvVars::UV_EXCLUDE_NEWER); + cmd.arg("--index-url").arg(packse_index_url()); + uv_snapshot!(filters, cmd, @" + 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 + revision = 3 + requires-python = ">=3.12" + required-markers = [ + "sys_platform == 'linux'", + "sys_platform == 'win32'", + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "package-win-only", marker = "sys_platform == 'win32'" }, + ] + + [package.metadata] + requires-dist = [{ name = "package-win-only", marker = "sys_platform == 'win32'" }] + + [[package]] + name = "package-win-only" + version = "1.0.0" + source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" } + wheels = [ + { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/requires_python_subset_win_only-1.0.0-cp312-abi3-win_amd64.whl", hash = "sha256:91d59021b1c4aad7449e315ae1248c5c588a7e84cb7592671a41453012302711" }, + ] + "# + ); + }); + + // 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(()) +} + /// When a dependency is only required on a specific platform (like x86_64), omit wheels that target other platforms (like aarch64). /// /// ```text diff --git a/crates/uv/tests/it/pip_compile_scenarios.rs b/crates/uv/tests/it/pip_compile_scenarios.rs index 52fcf00e86..17ad35fbe9 100644 --- a/crates/uv/tests/it/pip_compile_scenarios.rs +++ b/crates/uv/tests/it/pip_compile_scenarios.rs @@ -1,10 +1,11 @@ //! DO NOT EDIT //! //! Generated with `./scripts/sync_scenarios.sh` -//! Scenarios from +//! Scenarios from //! #![cfg(all(feature = "test-python", feature = "test-pypi", unix))] +use std::env; use std::process::Command; use anyhow::Result; @@ -352,7 +353,7 @@ fn incompatible_python_compatible_override() -> Result<()> { /// ```text /// python-patch-override-no-patch /// ├── environment -/// │ └── python3.9.20 +/// │ └── python3.9.21 /// ├── root /// │ └── requires a==1.0.0 /// │ └── satisfied by a-1.0.0 @@ -363,7 +364,7 @@ fn incompatible_python_compatible_override() -> Result<()> { #[cfg(feature = "test-python-patch")] #[test] fn python_patch_override_no_patch() -> Result<()> { - let context = uv_test::test_context!("3.9.20"); + let context = uv_test::test_context!("3.9.21"); let python_versions = &[]; // In addition to the standard filters, swap out package names for shorter messages @@ -376,18 +377,18 @@ fn python_patch_override_no_patch() -> Result<()> { // Since the resolver is asked to solve with 3.9, the minimum compatible Python requirement is treated as 3.9.0. let output = uv_snapshot!(filters, command(&context, python_versions) .arg("--python-version=3.9") - , @r" - success: false - exit_code: 1 - ----- stdout ----- + , @" + success: false + exit_code: 1 + ----- stdout ----- - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because the requested Python version (>=3.9) does not satisfy Python>=3.9.4 and package-a==1.0.0 depends on Python>=3.9.4, 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. + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because the requested Python version (>=3.9) does not satisfy Python>=3.9.4 and package-a==1.0.0 depends on Python>=3.9.4, 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. - hint: The `--python-version` value (>=3.9) includes Python versions that are not supported by your dependencies (e.g., package-a==1.0.0 only supports >=3.9.4). Consider using a higher `--python-version` value. - " + hint: The `--python-version` value (>=3.9) includes Python versions that are not supported by your dependencies (e.g., package-a==1.0.0 only supports >=3.9.4). Consider using a higher `--python-version` value. + " ); output.assert().failure(); @@ -400,7 +401,7 @@ fn python_patch_override_no_patch() -> Result<()> { /// ```text /// python-patch-override-patch-compatible /// ├── environment -/// │ └── python3.9.20 +/// │ └── python3.9.21 /// ├── root /// │ └── requires a==1.0.0 /// │ └── satisfied by a-1.0.0 @@ -411,7 +412,7 @@ fn python_patch_override_no_patch() -> Result<()> { #[cfg(feature = "test-python-patch")] #[test] fn python_patch_override_patch_compatible() -> Result<()> { - let context = uv_test::test_context!("3.9.20"); + let context = uv_test::test_context!("3.9.21"); let python_versions = &[]; // In addition to the standard filters, swap out package names for shorter messages @@ -423,19 +424,19 @@ fn python_patch_override_patch_compatible() -> Result<()> { let output = uv_snapshot!(filters, command(&context, python_versions) .arg("--python-version=3.9.0") - , @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.9.0 - package-a==1.0.0 - # via -r requirements.in + , @" + 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.9.0 + package-a==1.0.0 + # via -r requirements.in - ----- stderr ----- - warning: The requested Python version 3.9.0 is not available; 3.9.20 will be used to build dependencies instead. - Resolved 1 package in [TIME] - " + ----- stderr ----- + warning: The requested Python version 3.9.0 is not available; 3.9.21 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 4b9c0c5d0b..b748e5e79d 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 = "test-python", feature = "test-pypi", unix))] @@ -23,6 +23,119 @@ fn command(context: &TestContext) -> Command { command } +/// There are two packages, `a` and `b`. All versions of `b` require a specific +/// version of `a`, but that version requires a package `c` that does not exist. The resolver +/// must backtrack through all versions of `b` and eventually fail because no solution exists. +/// +/// ```text +/// backtrack-to-missing-package +/// ├── environment +/// │ └── python3.12 +/// ├── root +/// │ ├── requires a +/// │ │ ├── satisfied by a-2.0.0 +/// │ │ └── satisfied by a-1.0.0 +/// │ └── requires b +/// │ ├── satisfied by b-1.0.0 +/// │ ├── satisfied by b-2.0.0 +/// │ └── satisfied by b-3.0.0 +/// ├── a +/// │ ├── a-2.0.0 +/// │ └── a-1.0.0 +/// │ └── requires c +/// │ └── unsatisfied: no versions for package +/// └── b +/// ├── b-1.0.0 +/// │ └── requires a==1.0.0 +/// │ └── satisfied by a-1.0.0 +/// ├── b-2.0.0 +/// │ └── requires a==1.0.0 +/// │ └── satisfied by a-1.0.0 +/// └── b-3.0.0 +/// └── requires a==1.0.0 +/// └── satisfied by a-1.0.0 +/// ``` +#[test] +fn backtrack_to_missing_package() { + let context = uv_test::test_context!("3.12"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"backtrack-to-missing-package-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("backtrack-to-missing-package-a") + .arg("backtrack-to-missing-package-b") + , @" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because package-c was not found in the package registry and package-a==1.0.0 depends on package-c, we can conclude that package-a==1.0.0 cannot be used. + And because all versions of package-b depend on package-a==1.0.0 and you require package-b, we can conclude that your requirements are unsatisfiable. + "); + + context.assert_not_installed("backtrack_to_missing_package_a"); + context.assert_not_installed("backtrack_to_missing_package_b"); +} + +/// There are two packages, `a` and `b`. The latest version of `b` requires +/// a specific version of `a`. The older version of `b` requires a package `c` that does not +/// exist. The resolver should backtrack on `a` (not `b`) to find a solution without needing +/// to try `b==1.0.0` which would fail due to the missing package. +/// +/// ```text +/// backtrack-with-missing-package +/// ├── environment +/// │ └── python3.12 +/// ├── root +/// │ ├── requires a +/// │ │ ├── satisfied by a-1.0.0 +/// │ │ └── satisfied by a-2.0.0 +/// │ └── requires b +/// │ ├── satisfied by b-1.0.0 +/// │ └── satisfied by b-2.0.0 +/// ├── a +/// │ ├── a-1.0.0 +/// │ └── a-2.0.0 +/// └── b +/// ├── b-1.0.0 +/// │ └── requires c +/// │ └── unsatisfied: no versions for package +/// └── b-2.0.0 +/// └── requires a==1.0.0 +/// └── satisfied by a-1.0.0 +/// ``` +#[test] +fn backtrack_with_missing_package() { + let context = uv_test::test_context!("3.12"); + + // In addition to the standard filters, swap out package names for shorter messages + let mut filters = context.filters(); + filters.push((r"backtrack-with-missing-package-", "package-")); + + uv_snapshot!(filters, command(&context) + .arg("backtrack-with-missing-package-a") + .arg("backtrack-with-missing-package-b") + , @" + 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 + "); + + context.assert_installed("backtrack_with_missing_package_a", "1.0.0"); + context.assert_installed("backtrack_with_missing_package_b", "2.0.0"); +} + /// The user requires an exact version of package `a` but only other versions exist /// /// ```text @@ -3408,7 +3521,7 @@ fn python_greater_than_current_patch() { uv_snapshot!(filters, command(&context) .arg("python-greater-than-current-patch-a==1.0.0") - , @r" + , @" success: false exit_code: 1 ----- stdout ----- diff --git a/scripts/scenarios/generate.py b/scripts/scenarios/generate.py index 959eabb19d..592f072fab 100755 --- a/scripts/scenarios/generate.py +++ b/scripts/scenarios/generate.py @@ -51,7 +51,7 @@ PACKSE = TOOL_ROOT / "packse-scenarios" REQUIREMENTS = TOOL_ROOT / "pylock.toml" PROJECT_ROOT = TOOL_ROOT.parent.parent TESTS = PROJECT_ROOT / "crates" / "uv" / "tests" / "it" -TESTS_COMMON_MOD_RS = TESTS / "common" / "mod.rs" +TESTS_COMMON_MOD_RS = PROJECT_ROOT / "crates" / "uv-test" / "src" / "lib.rs" try: import packse @@ -185,7 +185,7 @@ def main( resolver_options = scenario["resolver_options"] or {} # Avoid writing the empty `required-environments = []` resolver_options["has_required_environments"] = bool( - resolver_options["required_environments"] + resolver_options.get("required_environments", []) ) if resolver_options.get("universal"): lock_scenarios.append(scenario) @@ -259,7 +259,7 @@ def main( "insta", "test", "--features", - "pypi,python,python-patch", + "test-pypi,test-python,test-python-patch", "--accept", "--test-runner", "nextest", diff --git a/scripts/scenarios/pylock.toml b/scripts/scenarios/pylock.toml index f55d1cc15c..7a51ed6840 100644 --- a/scripts/scenarios/pylock.toml +++ b/scripts/scenarios/pylock.toml @@ -66,9 +66,9 @@ wheels = [{ url = "https://files.pythonhosted.org/packages/20/12/38679034af33278 [[packages]] name = "packse" -version = "0.3.53" -sdist = { url = "https://files.pythonhosted.org/packages/52/58/373b6281bb741e875893dc351ac5f180c3fdce18a3b889f773725ff964b2/packse-0.3.53.tar.gz", upload-time = 2025-09-16T09:37:55Z, size = 5879063, hashes = { sha256 = "fcdbbb60f8ad4af94901891699a95ade4f15b9e769b4d8f443a2f3ef7aa74067" } } -wheels = [{ url = "https://files.pythonhosted.org/packages/fc/86/d5482bb2933fe47d282b1dae74cc9084b094f28229848ba8ea01a77fe0da/packse-0.3.53-py3-none-any.whl", upload-time = 2025-09-16T09:37:53Z, size = 34039, hashes = { sha256 = "78cf05f5e0b916f4070a66f04f3b371e2d4ac0c3917f38cb692a33fa6e9d764b" } }] +version = "0.3.59" +sdist = { url = "https://files.pythonhosted.org/packages/16/90/51404d8933506bd9554f607f5054f4715e0a5e1d34e4c6542580553e8b75/packse-0.3.59.tar.gz", upload-time = 2026-02-18T17:44:17Z, size = 5880109, hashes = { sha256 = "718bcca5dd1e9321f5c2918d5975ffd772f0c3b150cf29b03979677a003d73d2" } } +wheels = [{ url = "https://files.pythonhosted.org/packages/8d/95/b4a997b57a1f46f0f7575c1c021ee15e8fc1470b6ce74010239079d1d9aa/packse-0.3.59-py3-none-any.whl", upload-time = 2026-02-18T17:44:16Z, size = 34107, hashes = { sha256 = "10a3689ce0c00805cd7f1589862ab2d8f9fd4ab38c117342c351bfb13daa5c69" } }] [[packages]] name = "pathspec" diff --git a/scripts/scenarios/pyproject.toml b/scripts/scenarios/pyproject.toml index fd49a4345e..869a09e06f 100644 --- a/scripts/scenarios/pyproject.toml +++ b/scripts/scenarios/pyproject.toml @@ -1,5 +1,5 @@ [dependency-groups] packse = [ "chevron-blue", - "packse>=0.3.53" + "packse>=0.3.59" ] diff --git a/scripts/scenarios/templates/compile.mustache b/scripts/scenarios/templates/compile.mustache index ede4bd53b5..0955df3bfb 100644 --- a/scripts/scenarios/templates/compile.mustache +++ b/scripts/scenarios/templates/compile.mustache @@ -24,7 +24,7 @@ use uv_test::{ fn command(context: &TestContext, python_versions: &[&str]) -> Command { let python_path = python_path_with_versions(&context.temp_dir, python_versions) .expect("Failed to create Python test path"); - let mut command = Command::new(get_bin()); + let mut command = Command::new(get_bin!()); command .arg("pip") .arg("compile") diff --git a/scripts/sync-python-version-constants.py b/scripts/sync-python-version-constants.py index b19c5534f4..8c85dd7045 100644 --- a/scripts/sync-python-version-constants.py +++ b/scripts/sync-python-version-constants.py @@ -2,7 +2,7 @@ This script reads the download-metadata.json file and extracts the latest patch version for each minor version (3.15, 3.14, 3.13, 3.12, 3.11, 3.10). -It then updates the LATEST_PYTHON_X_Y constants in crates/uv/tests/it/common/mod.rs. +It then updates the LATEST_PYTHON_X_Y constants in crates/uv-test/src/lib.rs. For minor versions with stable releases, it uses the latest stable version. For minor versions with only prereleases, it uses the latest prerelease. @@ -70,7 +70,7 @@ def main() -> None: if minor not in latest_versions: latest_versions[minor] = prerelease_versions[minor] - # Update the constants in common/mod.rs + # Update the constants in uv-test/src/lib.rs lib_path = ROOT / "crates" / "uv-test" / "src" / "lib.rs" content = lib_path.read_text()