From daa3855b2ee1166dad33100386eda38eb5e17076 Mon Sep 17 00:00:00 2001 From: Liam Date: Wed, 1 Oct 2025 13:34:23 -0400 Subject: [PATCH 1/8] Error on dependency groups with incompatible requires-python in `pip compile` --- .../src/metadata/dependency_groups.rs | 36 ++++++++++++++++--- crates/uv/src/commands/pip/operations.rs | 31 ++++++++++++---- crates/uv/tests/it/pip_compile.rs | 32 +++++++++++++++++ 3 files changed, 88 insertions(+), 11 deletions(-) diff --git a/crates/uv-distribution/src/metadata/dependency_groups.rs b/crates/uv-distribution/src/metadata/dependency_groups.rs index d12e0651d..61bbb78bf 100644 --- a/crates/uv-distribution/src/metadata/dependency_groups.rs +++ b/crates/uv-distribution/src/metadata/dependency_groups.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use uv_configuration::SourceStrategy; -use uv_distribution_types::{IndexLocations, Requirement}; +use uv_distribution_types::{IndexLocations, Requirement, RequiresPython}; use uv_normalize::{GroupName, PackageName}; use uv_workspace::dependency_groups::FlatDependencyGroups; use uv_workspace::pyproject::{Sources, ToolUvSources}; @@ -45,7 +45,13 @@ use crate::metadata::{GitWorkspaceMember, LoweredRequirement, MetadataError}; #[derive(Debug, Clone)] pub struct SourcedDependencyGroups { pub name: Option, - pub dependency_groups: BTreeMap>, + pub dependency_groups: BTreeMap, +} + +#[derive(Debug, Clone)] +pub struct SourcedDependencyGroup { + pub requirements: Box<[Requirement]>, + pub requires_python: Option, } impl SourcedDependencyGroups { @@ -98,12 +104,23 @@ impl SourcedDependencyGroups { dependency_groups: dependency_groups .into_iter() .map(|(name, group)| { + let requires_python = group + .requires_python + .as_ref() + .map(RequiresPython::from_specifiers); + let requirements = group .requirements .into_iter() .map(Requirement::from) .collect(); - (name, requirements) + ( + name, + SourcedDependencyGroup { + requirements, + requires_python, + }, + ) }) .collect(), }); @@ -138,6 +155,11 @@ impl SourcedDependencyGroups { let dependency_groups = dependency_groups .into_iter() .map(|(name, group)| { + let requires_python = group + .requires_python + .as_ref() + .map(RequiresPython::from_specifiers); + let requirements = group .requirements .into_iter() @@ -167,7 +189,13 @@ impl SourcedDependencyGroups { }) }) .collect::, _>>()?; - Ok::<(GroupName, Box<_>), MetadataError>((name, requirements)) + Ok::<(GroupName, SourcedDependencyGroup), MetadataError>(( + name, + SourcedDependencyGroup { + requirements, + requires_python, + }, + )) }) .collect::, _>>()?; diff --git a/crates/uv/src/commands/pip/operations.rs b/crates/uv/src/commands/pip/operations.rs index 118606ab8..f37b94409 100644 --- a/crates/uv/src/commands/pip/operations.rs +++ b/crates/uv/src/commands/pip/operations.rs @@ -235,13 +235,30 @@ pub(crate) async fn resolve( // Apply dependency-groups for (group_name, group) in &metadata.dependency_groups { if groups.contains(group_name) { - requirements.extend(group.iter().cloned().map(|group| Requirement { - origin: Some(RequirementOrigin::Group( - pyproject_path.clone(), - metadata.name.clone(), - group_name.clone(), - )), - ..group + if let Some(requires_python) = group.requires_python.as_ref() { + if !python_requirement + .target() + .is_contained_by(requires_python.specifiers()) + { + return Err(anyhow!( + "Dependency group `{group_name}` in `{}` requires Python `{}`, but the active Python requirement is `{}`", + pyproject_path.user_display(), + requires_python.specifiers(), + python_requirement.target().specifiers() + ) + .into()); + } + } + + requirements.extend(group.requirements.iter().cloned().map(|group| { + Requirement { + origin: Some(RequirementOrigin::Group( + pyproject_path.clone(), + metadata.name.clone(), + group_name.clone(), + )), + ..group + } })); } } diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 8a7b9f8fb..f7aa19d5f 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -16070,6 +16070,38 @@ fn project_and_group_workspace() -> Result<()> { Ok(()) } +#[test] +fn group_requires_python_incompatible() -> Result<()> { + let context = TestContext::new("3.13"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "requires-python" + version = "0.1.0" + + [tool.uv.dependency-groups.ml1] + requires-python = ">=3.14" + + [dependency-groups] + ml1 = ["tqdm"] + "#, + )?; + + uv_snapshot!(context.filters(), context.pip_compile() + .arg("--group").arg("pyproject.toml:ml1"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Dependency group `ml1` in `pyproject.toml` requires Python `>=3.14`, but the active Python requirement is `>=3.13.[X]` + "); + + Ok(()) +} + #[test] fn directory_and_group() -> Result<()> { // Checking that --directory is handled properly with --group From bd45648cbed8e51eed52196bf9e6a615f82d75db Mon Sep 17 00:00:00 2001 From: Liam Date: Wed, 1 Oct 2025 17:20:06 -0400 Subject: [PATCH 2/8] Add source hint, current interpreter version, and CTA --- crates/uv-resolver/src/lib.rs | 2 +- crates/uv/src/commands/pip/operations.rs | 72 +++++++++++++++++++++--- crates/uv/tests/it/pip_compile.rs | 2 +- 3 files changed, 66 insertions(+), 10 deletions(-) diff --git a/crates/uv-resolver/src/lib.rs b/crates/uv-resolver/src/lib.rs index 00cb9732e..b37b291ce 100644 --- a/crates/uv-resolver/src/lib.rs +++ b/crates/uv-resolver/src/lib.rs @@ -15,7 +15,7 @@ pub use manifest::Manifest; pub use options::{Flexibility, Options, OptionsBuilder}; pub use preferences::{Preference, PreferenceError, Preferences}; pub use prerelease::PrereleaseMode; -pub use python_requirement::PythonRequirement; +pub use python_requirement::{PythonRequirement, PythonRequirementSource}; pub use resolution::{ AnnotationStyle, ConflictingDistributionError, DisplayResolutionGraph, ResolverOutput, }; diff --git a/crates/uv/src/commands/pip/operations.rs b/crates/uv/src/commands/pip/operations.rs index f37b94409..473f5d2b1 100644 --- a/crates/uv/src/commands/pip/operations.rs +++ b/crates/uv/src/commands/pip/operations.rs @@ -27,6 +27,7 @@ use uv_fs::Simplified; use uv_install_wheel::LinkMode; use uv_installer::{InstallationStrategy, Plan, Planner, Preparer, SitePackages}; use uv_normalize::PackageName; +use uv_pep440::Operator; use uv_pep508::{MarkerEnvironment, RequirementOrigin}; use uv_platform_tags::Tags; use uv_preview::Preview; @@ -38,7 +39,8 @@ use uv_requirements::{ }; use uv_resolver::{ DependencyMode, Exclusions, FlatIndex, InMemoryIndex, Manifest, Options, Preference, - Preferences, PythonRequirement, Resolver, ResolverEnvironment, ResolverOutput, + Preferences, PythonRequirement, PythonRequirementSource, Resolver, ResolverEnvironment, + ResolverOutput, }; use uv_tool::InstalledTools; use uv_types::{BuildContext, HashStrategy, InFlight, InstalledPackagesProvider}; @@ -240,13 +242,67 @@ pub(crate) async fn resolve( .target() .is_contained_by(requires_python.specifiers()) { - return Err(anyhow!( - "Dependency group `{group_name}` in `{}` requires Python `{}`, but the active Python requirement is `{}`", - pyproject_path.user_display(), - requires_python.specifiers(), - python_requirement.target().specifiers() - ) - .into()); + let required_spec = requires_python.specifiers().to_string(); + let active_spec = python_requirement.target().specifiers().to_string(); + let interpreter_version = python_requirement.exact(); + let interpreter_display = interpreter_version.to_string(); + + let suggested_version = requires_python + .range() + .lower() + .specifier() + .and_then(|specifier| { + let (operator, version) = specifier.into_parts(); + + match operator { + Operator::GreaterThan + | Operator::GreaterThanEqual + | Operator::Equal + | Operator::ExactEqual + | Operator::EqualStar + | Operator::TildeEqual => Some(version), + _ => None, + } + }); + + let source_hint = match python_requirement.source() { + PythonRequirementSource::PythonVersion => " (set via `--python`)", + PythonRequirementSource::RequiresPython => { + " (set via this project's `requires-python`)" + } + PythonRequirementSource::Interpreter => "", + }; + + let mut message = format!( + "Dependency group `{group_name}` in `{}` requires Python `{required_spec}`, but uv is resolving for Python `{active_spec}`{source_hint} (current interpreter: `{interpreter_display}`).", + pyproject_path.user_display() + ); + + let call_to_action = if let Some(ref version) = suggested_version { + let suggested_python = version.to_string(); + + if interpreter_version >= version + && matches!( + python_requirement.source(), + PythonRequirementSource::PythonVersion + ) + { + format!( + " Drop `--python` to use your current interpreter or re-run with `--python {suggested_python}` to target a compatible Python version." + ) + } else { + format!( + " Re-run with `--python {suggested_python}` to target a compatible Python version." + ) + } + } else { + " Specify a compatible Python version with `--python `." + .to_string() + }; + + message.push_str(&call_to_action); + + return Err(anyhow!(message).into()); } } diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index f7aa19d5f..5665481ba 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -16096,7 +16096,7 @@ fn group_requires_python_incompatible() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: Dependency group `ml1` in `pyproject.toml` requires Python `>=3.14`, but the active Python requirement is `>=3.13.[X]` + error: Dependency group `ml1` in `pyproject.toml` requires Python `>=3.14`, but uv is resolving for Python `>=3.13.[X]` (current interpreter: `3.13.[X]`). Re-run with `--python 3.14` to target a compatible Python version. "); Ok(()) From c0741d5d106bfb53aae0447a7c3e7400a0f7db44 Mon Sep 17 00:00:00 2001 From: Liam Date: Wed, 1 Oct 2025 17:29:21 -0400 Subject: [PATCH 3/8] Add test for python flag --- crates/uv/tests/it/pip_compile.rs | 36 ++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 5665481ba..d3ce7bc08 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -16071,7 +16071,7 @@ fn project_and_group_workspace() -> Result<()> { } #[test] -fn group_requires_python_incompatible() -> Result<()> { +fn group_requires_python_incompatible_with_interpreter() -> Result<()> { let context = TestContext::new("3.13"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -16102,6 +16102,40 @@ fn group_requires_python_incompatible() -> Result<()> { Ok(()) } +#[test] +fn group_requires_python_incompatible_with_python_flag() -> Result<()> { + let context = TestContext::new("3.13"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "requires-python" + version = "0.1.0" + + [tool.uv.dependency-groups.ml1] + requires-python = ">=3.13" + + [dependency-groups] + ml1 = ["tqdm"] + "#, + )?; + + uv_snapshot!(context.filters(), context.pip_compile() + .arg("--group").arg("pyproject.toml:ml1") + .arg("--python").arg("3.12"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: The requested Python version 3.12 is not available; 3.13.[X] will be used to build dependencies instead. + error: Dependency group `ml1` in `pyproject.toml` requires Python `>=3.13`, but uv is resolving for Python `>=3.12` (set via `--python`) (current interpreter: `3.13.[X]`). Drop `--python` to use your current interpreter or re-run with `--python 3.13` to target a compatible Python version. + "); + + Ok(()) +} + #[test] fn directory_and_group() -> Result<()> { // Checking that --directory is handled properly with --group From 70aad0ad868139d289664cce391ac74bec31cffc Mon Sep 17 00:00:00 2001 From: Liam Date: Wed, 1 Oct 2025 18:12:57 -0400 Subject: [PATCH 4/8] Drop source hint, fix potential version error --- crates/uv/src/commands/pip/operations.rs | 42 +++++------------------- crates/uv/tests/it/pip_compile.rs | 2 +- 2 files changed, 9 insertions(+), 35 deletions(-) diff --git a/crates/uv/src/commands/pip/operations.rs b/crates/uv/src/commands/pip/operations.rs index 473f5d2b1..536b9b5e8 100644 --- a/crates/uv/src/commands/pip/operations.rs +++ b/crates/uv/src/commands/pip/operations.rs @@ -39,8 +39,7 @@ use uv_requirements::{ }; use uv_resolver::{ DependencyMode, Exclusions, FlatIndex, InMemoryIndex, Manifest, Options, Preference, - Preferences, PythonRequirement, PythonRequirementSource, Resolver, ResolverEnvironment, - ResolverOutput, + Preferences, PythonRequirement, Resolver, ResolverEnvironment, ResolverOutput, }; use uv_tool::InstalledTools; use uv_types::{BuildContext, HashStrategy, InFlight, InstalledPackagesProvider}; @@ -255,8 +254,7 @@ pub(crate) async fn resolve( let (operator, version) = specifier.into_parts(); match operator { - Operator::GreaterThan - | Operator::GreaterThanEqual + Operator::GreaterThanEqual | Operator::Equal | Operator::ExactEqual | Operator::EqualStar @@ -265,42 +263,18 @@ pub(crate) async fn resolve( } }); - let source_hint = match python_requirement.source() { - PythonRequirementSource::PythonVersion => " (set via `--python`)", - PythonRequirementSource::RequiresPython => { - " (set via this project's `requires-python`)" - } - PythonRequirementSource::Interpreter => "", - }; - let mut message = format!( - "Dependency group `{group_name}` in `{}` requires Python `{required_spec}`, but uv is resolving for Python `{active_spec}`{source_hint} (current interpreter: `{interpreter_display}`).", + "Dependency group `{group_name}` in `{}` requires Python `{required_spec}`, but uv is resolving for Python `{active_spec}` (current interpreter: `{interpreter_display}`).", pyproject_path.user_display() ); - let call_to_action = if let Some(ref version) = suggested_version { + if let Some(ref version) = suggested_version { let suggested_python = version.to_string(); - if interpreter_version >= version - && matches!( - python_requirement.source(), - PythonRequirementSource::PythonVersion - ) - { - format!( - " Drop `--python` to use your current interpreter or re-run with `--python {suggested_python}` to target a compatible Python version." - ) - } else { - format!( - " Re-run with `--python {suggested_python}` to target a compatible Python version." - ) - } - } else { - " Specify a compatible Python version with `--python `." - .to_string() - }; - - message.push_str(&call_to_action); + message.push_str(&format!( + " Re-run with `--python {suggested_python}` to target a compatible Python version." + )) + } return Err(anyhow!(message).into()); } diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index d3ce7bc08..41bc83444 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -16130,7 +16130,7 @@ fn group_requires_python_incompatible_with_python_flag() -> Result<()> { ----- stderr ----- warning: The requested Python version 3.12 is not available; 3.13.[X] will be used to build dependencies instead. - error: Dependency group `ml1` in `pyproject.toml` requires Python `>=3.13`, but uv is resolving for Python `>=3.12` (set via `--python`) (current interpreter: `3.13.[X]`). Drop `--python` to use your current interpreter or re-run with `--python 3.13` to target a compatible Python version. + error: Dependency group `ml1` in `pyproject.toml` requires Python `>=3.13`, but uv is resolving for Python `>=3.12` (current interpreter: `3.13.[X]`). Re-run with `--python 3.13` to target a compatible Python version. "); Ok(()) From 4ab1fe10e71fd4c03b09b37f9ed24d9f16d1ebc0 Mon Sep 17 00:00:00 2001 From: Liam Date: Wed, 1 Oct 2025 18:27:22 -0400 Subject: [PATCH 5/8] Appease clippy --- crates/uv/src/commands/pip/operations.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/uv/src/commands/pip/operations.rs b/crates/uv/src/commands/pip/operations.rs index 536b9b5e8..e2e327a8a 100644 --- a/crates/uv/src/commands/pip/operations.rs +++ b/crates/uv/src/commands/pip/operations.rs @@ -271,9 +271,9 @@ pub(crate) async fn resolve( if let Some(ref version) = suggested_version { let suggested_python = version.to_string(); - message.push_str(&format!( + write!(message, "{}", &format!( " Re-run with `--python {suggested_python}` to target a compatible Python version." - )) + ))?; } return Err(anyhow!(message).into()); From 196d420eb5c40a084ac57ac930fcf23b69482b7c Mon Sep 17 00:00:00 2001 From: Liam Date: Wed, 1 Oct 2025 22:19:05 -0400 Subject: [PATCH 6/8] Format --- crates/uv/src/commands/pip/operations.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/crates/uv/src/commands/pip/operations.rs b/crates/uv/src/commands/pip/operations.rs index e2e327a8a..1f1c8bfaf 100644 --- a/crates/uv/src/commands/pip/operations.rs +++ b/crates/uv/src/commands/pip/operations.rs @@ -271,9 +271,13 @@ pub(crate) async fn resolve( if let Some(ref version) = suggested_version { let suggested_python = version.to_string(); - write!(message, "{}", &format!( - " Re-run with `--python {suggested_python}` to target a compatible Python version." - ))?; + write!( + message, + "{}", + &format!( + " Re-run with `--python {suggested_python}` to target a compatible Python version." + ) + )?; } return Err(anyhow!(message).into()); From 3adab8e172ce89878d7b6076e6c5caaf7a12119c Mon Sep 17 00:00:00 2001 From: Liam Date: Wed, 1 Oct 2025 23:03:11 -0400 Subject: [PATCH 7/8] Inline interpreter version --- crates/uv/src/commands/pip/operations.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/uv/src/commands/pip/operations.rs b/crates/uv/src/commands/pip/operations.rs index 1f1c8bfaf..bd699c8f2 100644 --- a/crates/uv/src/commands/pip/operations.rs +++ b/crates/uv/src/commands/pip/operations.rs @@ -243,8 +243,7 @@ pub(crate) async fn resolve( { let required_spec = requires_python.specifiers().to_string(); let active_spec = python_requirement.target().specifiers().to_string(); - let interpreter_version = python_requirement.exact(); - let interpreter_display = interpreter_version.to_string(); + let interpreter_version = python_requirement.exact().to_string(); let suggested_version = requires_python .range() @@ -264,7 +263,7 @@ pub(crate) async fn resolve( }); let mut message = format!( - "Dependency group `{group_name}` in `{}` requires Python `{required_spec}`, but uv is resolving for Python `{active_spec}` (current interpreter: `{interpreter_display}`).", + "Dependency group `{group_name}` in `{}` requires Python `{required_spec}`, but uv is resolving for Python `{active_spec}` (current interpreter: `{interpreter_version}`).", pyproject_path.user_display() ); From 8f6b1f528f2f6fe5c671c51352f34eb912b7490e Mon Sep 17 00:00:00 2001 From: Liam Date: Thu, 2 Oct 2025 11:07:54 -0400 Subject: [PATCH 8/8] Make python version available in test context --- crates/uv/tests/it/pip_compile.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 41bc83444..296ca76f9 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -16104,7 +16104,7 @@ fn group_requires_python_incompatible_with_interpreter() -> Result<()> { #[test] fn group_requires_python_incompatible_with_python_flag() -> Result<()> { - let context = TestContext::new("3.13"); + let context = TestContext::new_with_versions(&["3.12", "3.13"]); let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str( @@ -16129,8 +16129,7 @@ fn group_requires_python_incompatible_with_python_flag() -> Result<()> { ----- stdout ----- ----- stderr ----- - warning: The requested Python version 3.12 is not available; 3.13.[X] will be used to build dependencies instead. - error: Dependency group `ml1` in `pyproject.toml` requires Python `>=3.13`, but uv is resolving for Python `>=3.12` (current interpreter: `3.13.[X]`). Re-run with `--python 3.13` to target a compatible Python version. + error: Dependency group `ml1` in `pyproject.toml` requires Python `>=3.13`, but uv is resolving for Python `>=3.12` (current interpreter: `3.12.[X]`). Re-run with `--python 3.13` to target a compatible Python version. "); Ok(())