diff --git a/crates/uv-requirements/src/pyproject.rs b/crates/uv-requirements/src/pyproject.rs index ca92cc5d5..5904f486c 100644 --- a/crates/uv-requirements/src/pyproject.rs +++ b/crates/uv-requirements/src/pyproject.rs @@ -27,6 +27,7 @@ use uv_configuration::PreviewMode; use uv_fs::Simplified; use uv_git::GitReference; use uv_normalize::{ExtraName, PackageName}; +use uv_warnings::warn_user_once; use crate::ExtrasSpecification; @@ -280,6 +281,7 @@ impl Pep621Metadata { let requirements = lower_requirements( &project.dependencies.unwrap_or_default(), &project.optional_dependencies.unwrap_or_default(), + &project.name, project_dir, &project_sources.unwrap_or_default(), workspace_sources, @@ -318,6 +320,7 @@ impl Pep621Metadata { pub(crate) fn lower_requirements( dependencies: &[String], optional_dependencies: &IndexMap>, + project_name: &PackageName, project_dir: &Path, project_sources: &HashMap, workspace_sources: &HashMap, @@ -331,6 +334,7 @@ pub(crate) fn lower_requirements( let name = requirement.name.clone(); lower_requirement( requirement, + project_name, project_dir, project_sources, workspace_sources, @@ -350,6 +354,7 @@ pub(crate) fn lower_requirements( let name = requirement.name.clone(); lower_requirement( requirement, + project_name, project_dir, project_sources, workspace_sources, @@ -371,6 +376,7 @@ pub(crate) fn lower_requirements( /// Combine `project.dependencies` or `project.optional-dependencies` with `tool.uv.sources`. pub(crate) fn lower_requirement( requirement: pep508_rs::Requirement, + project_name: &PackageName, project_dir: &Path, project_sources: &HashMap, workspace_sources: &HashMap, @@ -395,7 +401,15 @@ pub(crate) fn lower_requirement( } let Some(source) = source else { + let has_sources = !project_sources.is_empty() || !workspace_sources.is_empty(); // Support recursive editable inclusions. + if has_sources && requirement.version_or_url.is_none() && &requirement.name != project_name + { + warn_user_once!( + "Missing version constraint (e.g., a lower bound) for `{}`", + requirement.name + ); + } return Ok(Requirement::from_pep508(requirement)?); }; @@ -476,10 +490,16 @@ pub(crate) fn lower_requirement( path_source(path, project_dir, editable)? } Source::Registry { index } => match requirement.version_or_url { - None => RequirementSource::Registry { - specifier: VersionSpecifiers::empty(), - index: Some(index), - }, + None => { + warn_user_once!( + "Missing version constraint (e.g., a lower bound) for `{}`", + requirement.name + ); + RequirementSource::Registry { + specifier: VersionSpecifiers::empty(), + index: Some(index), + } + } Some(VersionOrUrl::VersionSpecifier(version)) => RequirementSource::Registry { specifier: version, index: Some(index), @@ -630,8 +650,8 @@ mod test { use anyhow::Context; use indoc::indoc; use insta::assert_snapshot; - use uv_configuration::PreviewMode; + use uv_configuration::PreviewMode; use uv_fs::Simplified; use crate::{ExtrasSpecification, RequirementsSpecification}; @@ -756,6 +776,20 @@ mod test { "###); } + #[test] + fn missing_constraint() { + let input = indoc! {r#" + [project] + name = "foo" + version = "0.0.0" + dependencies = [ + "tqdm", + ] + "#}; + + assert!(from_source(input, "pyproject.toml", &ExtrasSpecification::None).is_ok()); + } + #[test] fn invalid_syntax() { let input = indoc! {r#" diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 0f02606ff..a88400452 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -8653,6 +8653,85 @@ fn git_source_missing_tag() -> Result<()> { Ok(()) } +#[test] +fn warn_missing_constraint() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "foo" + version = "0.0.0" + dependencies = [ + "tqdm", + "anyio==4.3.0", + ] + + [tool.uv.sources] + anyio = { url = "https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl" } + "#})?; + + uv_snapshot!(context.filters(), context.compile() + .arg("--preview") + .arg("pyproject.toml"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z --preview pyproject.toml + anyio @ https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl + idna==3.6 + # via anyio + sniffio==1.3.1 + # via anyio + tqdm==4.66.2 + + ----- stderr ----- + warning: Missing version constraint (e.g., a lower bound) for `tqdm` + Resolved 4 packages in [TIME] + "###); + + Ok(()) +} + +/// Ensure that this behavior is constraint to preview mode. +#[test] +fn dont_warn_missing_constraint_without_sources() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "foo" + version = "0.0.0" + dependencies = [ + "tqdm", + "anyio==4.3.0", + ] + "#})?; + + uv_snapshot!(context.filters(), context.compile() + .arg("--preview") + .arg("pyproject.toml"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z --preview pyproject.toml + anyio==4.3.0 + idna==3.6 + # via anyio + sniffio==1.3.1 + # via anyio + tqdm==4.66.2 + + ----- stderr ----- + Resolved 4 packages in [TIME] + "###); + + Ok(()) +} + #[test] fn tool_uv_sources() -> Result<()> { let context = TestContext::new("3.12");