Skip to content

Commit

Permalink
Warn when missing minimal bounds when using tool.uv.sources (#3452)
Browse files Browse the repository at this point in the history
When using `tool.uv.sources`, we warn that requirements have a bound,
i.e. at least a lower version constraint.

When using a library, the symbols you import were introduced in
different versions, creating an implicit lower bound. This warning makes
this explicit. This is crucial to prevent backtracking resolvers from
selecting an ancient versions that is not compatible (or worse, doesn't
build), and a performance optimization on top.

This feature is gated to `tool.uv.sources` (as it should have been to
begin with for #3263/#3443) to not unnecessarily break legacy workflows.
It is also helpful specifically when using a `tool.uv.sources` section
that contains constraints that are not published to pypi, e.g. for
workspace dependencies. We can adjust those later to e.g. not constrain
workspace dependencies with `publish = false`, but i think it's the
right setting to start with.
  • Loading branch information
konstin authored May 8, 2024
1 parent 7c66321 commit 7c7c9e2
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 5 deletions.
44 changes: 39 additions & 5 deletions crates/uv-requirements/src/pyproject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -318,6 +320,7 @@ impl Pep621Metadata {
pub(crate) fn lower_requirements(
dependencies: &[String],
optional_dependencies: &IndexMap<ExtraName, Vec<String>>,
project_name: &PackageName,
project_dir: &Path,
project_sources: &HashMap<PackageName, Source>,
workspace_sources: &HashMap<PackageName, Source>,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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<PackageName, Source>,
workspace_sources: &HashMap<PackageName, Source>,
Expand All @@ -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)?);
};

Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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};
Expand Down Expand Up @@ -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#"
Expand Down
79 changes: 79 additions & 0 deletions crates/uv/tests/pip_compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down

0 comments on commit 7c7c9e2

Please sign in to comment.