diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 22abe546e95f7..557084e7cdb0e 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -1230,36 +1230,21 @@ impl ResolverState Result<()> { Ok(()) } +/// Lock a project with a local path dependency gated behind a Python version marker, where the +/// child's `requires-python` is stricter than the root but compatible with the marker. +/// +/// See: +#[test] +fn lock_path_dependency_marker_gated_requires_python() -> Result<()> { + // Use Python 3.9 as the interpreter to reproduce the issue: the installed Python (3.9) + // does not satisfy the child's `requires-python = ">=3.12"`, but the child is gated + // behind `python_version >= '3.12'`, so it should still resolve. + let context = uv_test::test_context!("3.9"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.9" + dependencies = ["child; python_version >= '3.12'"] + + [tool.uv.sources] + child = { path = "./child" } + "#, + )?; + + let child = context.temp_dir.child("child"); + child.child("pyproject.toml").write_str( + r#" + [project] + name = "child" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + "#, + )?; + + // The lock should succeed: the child is only required for Python >= 3.12, so its + // `requires-python = ">=3.12"` is compatible with the fork. + uv_snapshot!(context.filters(), context.lock(), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + "); + + // Re-lock with `--refresh` should also succeed. + uv_snapshot!(context.filters(), context.lock().arg("--refresh"), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + "); + + Ok(()) +} + /// Test that a nested path dependency with an explicit index validates correctly. #[tokio::test] async fn lock_nested_path_dependency_explicit_index() -> Result<()> { diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index a932cb2701e2b..8577a5a9feae3 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -11067,18 +11067,7 @@ requires-python = ">=3.13" let requirements_in = context.temp_dir.child("requirements.in"); requirements_in.write_str(&format!("-e {}", editable_dir.path().display()))?; - let filters: Vec<_> = [ - // 3.11 may not be installed - ( - "warning: The requested Python version 3.11 is not available; .* will be used to build dependencies instead.\n", - "", - ), - ] - .into_iter() - .chain(context.filters()) - .collect(); - - uv_snapshot!(filters, context.pip_compile() + uv_snapshot!(context.filters(), context.pip_compile() .arg("requirements.in") .arg("--python-version=3.11"), @" success: false @@ -11086,9 +11075,65 @@ requires-python = ">=3.13" ----- stdout ----- ----- stderr ----- + warning: The requested Python version 3.11 is not available; 3.12.[X] will be used to build dependencies instead. × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.12.[X]) does not satisfy Python>=3.13 and example==0.0.0 depends on Python>=3.13, we can conclude that example==0.0.0 cannot be used. + ╰─▶ Because the requested Python version (>=3.11) does not satisfy Python>=3.13 and example==0.0.0 depends on Python>=3.13, we can conclude that example==0.0.0 cannot be used. And because only example==0.0.0 is available and you require example, we can conclude that your requirements are unsatisfiable. + + hint: The `--python-version` value (>=3.11) includes Python versions that are not supported by your dependencies (e.g., example==0.0.0 only supports >=3.13). Consider using a higher `--python-version` value. + " + ); + + Ok(()) +} + +/// Resolve successfully when an editable's `Requires-Python` is satisfied by +/// `--python-version` but not by the installed interpreter. +#[test] +fn requires_python_editable_installed_incompatible() -> Result<()> { + let context = uv_test::test_context!("3.12"); + + // Create an editable package with `requires-python >= 3.13`. + let editable_dir = context.temp_dir.child("editable"); + editable_dir.create_dir_all()?; + let pyproject_toml = editable_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#"[project] +name = "example" +version = "0.0.0" +dependencies = [ + "anyio==4.0.0" +] +requires-python = ">=3.13" +"#, + )?; + + // Write to a requirements file. + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str(&format!("-e {}", editable_dir.path().display()))?; + + // `--python-version 3.13` satisfies `requires-python >= 3.13`, so resolution succeeds + // even though the installed interpreter is 3.12. + uv_snapshot!(context.filters(), context.pip_compile() + .arg("requirements.in") + .arg("--python-version=3.13"), @" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] requirements.in --python-version=3.13 + -e [TEMP_DIR]/editable + # via -r requirements.in + anyio==4.0.0 + # via example + idna==3.6 + # via anyio + sniffio==1.3.1 + # via anyio + + ----- stderr ----- + warning: The requested Python version 3.13 is not available; 3.12.[X] will be used to build dependencies instead. + Resolved 4 packages in [TIME] " ); diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index cce9961c9bc6b..bac1c7ba2a3a5 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -5247,6 +5247,82 @@ requires-python = ">=3.13" Ok(()) } +/// Resolve successfully when `--python-version` satisfies `Requires-Python` but the installed +/// interpreter does not. The `installed()` check is not applied in the resolver — the resolution +/// target is what matters. +#[test] +fn requires_python_source_dist_installed_incompatible() -> Result<()> { + let context = uv_test::test_context!("3.12"); + + // Create a source distribution with `requires-python >= 3.13`. + let child_dir = context.temp_dir.child("child"); + child_dir.create_dir_all()?; + child_dir.child("pyproject.toml").write_str( + r#"[project] +name = "example" +version = "0.0.0" +dependencies = [] +requires-python = ">=3.13" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" +"#, + )?; + child_dir.child("src").child("example").create_dir_all()?; + child_dir + .child("src") + .child("example") + .child("__init__.py") + .touch()?; + + // `--python-version 3.13` satisfies `requires-python >= 3.13`, so resolution succeeds + // even though the installed interpreter is 3.12. + uv_snapshot!(context.filters(), context.pip_install() + .arg("--python-version=3.13") + .arg(child_dir.path()), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + example==0.0.0 (from file://[TEMP_DIR]/child) + " + ); + + Ok(()) +} + +/// Like [`requires_python_source_dist_installed_incompatible`], but using a registry package +/// (`iniconfig`) instead of a direct URL, with `--no-binary` to force building from source. +#[test] +fn requires_python_source_dist_installed_incompatible_registry() { + let context = uv_test::test_context!("3.9").with_exclude_newer("2025-11-01T00:00:00Z"); + + // `--python-version 3.10` satisfies `requires-python >= 3.10`, so resolution succeeds + // even though the installed interpreter is 3.9. This should arguably fail, and some build + // backends would likely error in this scenario. + uv_snapshot!(context.filters(), context.pip_install() + .arg("--python-version=3.10") + .arg("--no-binary") + .arg("iniconfig") + .arg("iniconfig==2.3.0"), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.3.0 + " + ); +} + /// Install with `--no-build-isolation`, to disable isolation during PEP 517 builds. #[test] fn no_build_isolation() -> Result<()> {