Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 6 additions & 21 deletions crates/uv-resolver/src/resolver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1230,36 +1230,21 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag

// The version is incompatible due to its `Requires-Python` requirement.
if let Some(requires_python) = metadata.requires_python.as_ref() {
// TODO(charlie): We only care about this for source distributions.
if !python_requirement
.installed()
.is_contained_by(requires_python)
{
return Ok(Some(ResolverVersion::Unavailable(
version.clone(),
UnavailableVersion::IncompatibleDist(IncompatibleDist::Source(
IncompatibleSource::RequiresPython(
requires_python.clone(),
PythonRequirementKind::Installed,
),
)),
)));
}
if !python_requirement.target().is_contained_by(requires_python) {
let kind = if python_requirement.installed() == python_requirement.target() {
PythonRequirementKind::Installed
} else {
PythonRequirementKind::Target
};
return Ok(Some(ResolverVersion::Unavailable(
version.clone(),
UnavailableVersion::IncompatibleDist(IncompatibleDist::Source(
IncompatibleSource::RequiresPython(
requires_python.clone(),
PythonRequirementKind::Target,
),
IncompatibleSource::RequiresPython(requires_python.clone(), kind),
)),
)));
}
}

// If this is a wheel, and the implied Python version doesn't overlap, raise an error.

Ok(Some(ResolverVersion::Unforked(version.clone())))
}

Expand Down
60 changes: 60 additions & 0 deletions crates/uv/tests/it/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32845,6 +32845,66 @@ fn lock_path_dependency_no_index() -> 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: <https://github.com/astral-sh/uv/issues/18199>
#[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<()> {
Expand Down
71 changes: 58 additions & 13 deletions crates/uv/tests/it/pip_compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11067,28 +11067,73 @@ 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
exit_code: 1
----- 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]
"
);

Expand Down
76 changes: 76 additions & 0 deletions crates/uv/tests/it/pip_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Expand Down
Loading