From 0242f435f8ac4e5b5aa66562bc5bcfd1aae4fb12 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 9 Dec 2024 12:16:08 -0500 Subject: [PATCH] Allow users to specify URLs in `project.dependencies` and `tool.uv.sources` (#9718) ## Summary This PR allows users to specify a source both in `project.dependencies` ("production") and `tool.uv.sources` ("development"). It's not intended as a holistic fix for "production" vs. "development" dependencies, but in some cases this is good enough with `--no-sources`, and I don't see a great reason for enforcing it right now. Closes: https://github.com/astral-sh/uv/issues/9682 Ref: https://github.com/astral-sh/uv/issues/7945 (but I'll leave this open?) --- .../uv-distribution/src/metadata/lowering.rs | 51 +++++------ .../src/metadata/requires_dist.rs | 21 +---- crates/uv/tests/it/lock.rs | 84 +++++++++++++++++-- 3 files changed, 99 insertions(+), 57 deletions(-) diff --git a/crates/uv-distribution/src/metadata/lowering.rs b/crates/uv-distribution/src/metadata/lowering.rs index 0e7cb1a5bbb3..3c4e30180cb0 100644 --- a/crates/uv-distribution/src/metadata/lowering.rs +++ b/crates/uv-distribution/src/metadata/lowering.rs @@ -185,9 +185,6 @@ impl LoweredRequirement { marker, .. } => { - if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) { - return Err(LoweringError::ConflictingUrls); - } let source = git_source( &git, subdirectory.map(PathBuf::from), @@ -203,9 +200,6 @@ impl LoweredRequirement { marker, .. } => { - if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) { - return Err(LoweringError::ConflictingUrls); - } let source = url_source(url, subdirectory.map(PathBuf::from))?; (source, marker) } @@ -215,9 +209,6 @@ impl LoweredRequirement { marker, .. } => { - if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) { - return Err(LoweringError::ConflictingUrls); - } let source = path_source( PathBuf::from(path), git_member, @@ -265,7 +256,7 @@ impl LoweredRequirement { index.into_url(), conflict, lower_bound, - )?; + ); (source, marker) } Source::Workspace { @@ -276,9 +267,6 @@ impl LoweredRequirement { if !is_workspace { return Err(LoweringError::WorkspaceFalse); } - if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) { - return Err(LoweringError::ConflictingUrls); - } let member = workspace .packages() .get(&requirement.name) @@ -433,9 +421,6 @@ impl LoweredRequirement { marker, .. } => { - if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) { - return Err(LoweringError::ConflictingUrls); - } let source = git_source( &git, subdirectory.map(PathBuf::from), @@ -451,9 +436,6 @@ impl LoweredRequirement { marker, .. } => { - if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) { - return Err(LoweringError::ConflictingUrls); - } let source = url_source(url, subdirectory.map(PathBuf::from))?; (source, marker) } @@ -463,9 +445,6 @@ impl LoweredRequirement { marker, .. } => { - if matches!(requirement.version_or_url, Some(VersionOrUrl::Url(_))) { - return Err(LoweringError::ConflictingUrls); - } let source = path_source( PathBuf::from(path), None, @@ -497,7 +476,7 @@ impl LoweredRequirement { index.into_url(), conflict, lower_bound, - )?; + ); (source, marker) } Source::Workspace { .. } => { @@ -550,8 +529,6 @@ pub enum LoweringError { InvalidUrl(#[from] url::ParseError), #[error(transparent)] InvalidVerbatimUrl(#[from] uv_pep508::VerbatimUrlError), - #[error("Can't combine URLs from both `project.dependencies` and `tool.uv.sources`")] - ConflictingUrls, #[error("Fragments are not allowed in URLs: `{0}`")] ForbiddenFragment(Url), #[error("`workspace = false` is not yet supported")] @@ -658,7 +635,7 @@ fn registry_source( index: Url, conflict: Option, bounds: LowerBound, -) -> Result { +) -> RequirementSource { match &requirement.version_or_url { None => { if matches!(bounds, LowerBound::Warn) { @@ -667,18 +644,30 @@ fn registry_source( requirement.name ); } - Ok(RequirementSource::Registry { + RequirementSource::Registry { specifier: VersionSpecifiers::empty(), index: Some(index), conflict, - }) + } } - Some(VersionOrUrl::VersionSpecifier(version)) => Ok(RequirementSource::Registry { + Some(VersionOrUrl::VersionSpecifier(version)) => RequirementSource::Registry { specifier: version.clone(), index: Some(index), conflict, - }), - Some(VersionOrUrl::Url(_)) => Err(LoweringError::ConflictingUrls), + }, + Some(VersionOrUrl::Url(_)) => { + if matches!(bounds, LowerBound::Warn) { + warn_user_once!( + "Missing version constraint (e.g., a lower bound) for `{}` due to use of a URL specifier", + requirement.name + ); + } + RequirementSource::Registry { + specifier: VersionSpecifiers::empty(), + index: Some(index), + conflict, + } + } } } diff --git a/crates/uv-distribution/src/metadata/requires_dist.rs b/crates/uv-distribution/src/metadata/requires_dist.rs index 0c33dfd12e6b..1b77e4f163f5 100644 --- a/crates/uv-distribution/src/metadata/requires_dist.rs +++ b/crates/uv-distribution/src/metadata/requires_dist.rs @@ -366,25 +366,6 @@ mod test { message } - #[tokio::test] - async fn conflict_project_and_sources() { - let input = indoc! {r#" - [project] - name = "foo" - version = "0.0.0" - dependencies = [ - "tqdm @ git+https://github.com/tqdm/tqdm", - ] - [tool.uv.sources] - tqdm = { url = "https://files.pythonhosted.org/packages/a5/d6/502a859bac4ad5e274255576cd3e15ca273cdb91731bc39fb840dd422ee9/tqdm-4.66.0-py3-none-any.whl" } - "#}; - - assert_snapshot!(format_err(input).await, @r###" - error: Failed to parse entry: `tqdm` - Caused by: Can't combine URLs from both `project.dependencies` and `tool.uv.sources` - "###); - } - #[tokio::test] async fn wrong_type() { let input = indoc! {r#" @@ -568,7 +549,7 @@ mod test { assert_snapshot!(format_err(input).await, @r###" error: Failed to parse entry: `tqdm` - Caused by: Can't combine URLs from both `project.dependencies` and `tool.uv.sources` + Caused by: `tqdm` references a workspace in `tool.uv.sources` (e.g., `tqdm = { workspace = true }`), but is not a workspace member "###); } diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 38c5e8900780..f0956f851ed4 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -9738,7 +9738,7 @@ fn lock_transitive_extra() -> Result<()> { } /// If a source is provided via `tool.uv.sources` _and_ a URL is provided in `project.dependencies`, -/// we raise an error. +/// we accept the source in `tool.uv.sources`, unless `--no-sources` is provided. #[test] fn lock_mismatched_sources() -> Result<()> { let context = TestContext::new("3.12"); @@ -9760,16 +9760,88 @@ fn lock_mismatched_sources() -> Result<()> { )?; uv_snapshot!(context.filters(), context.lock(), @r###" - success: false - exit_code: 1 + success: true + exit_code: 0 ----- stdout ----- ----- stderr ----- - × Failed to build `project @ file://[TEMP_DIR]/` - ├─▶ Failed to parse entry: `uv-public-pypackage` - ╰─▶ Can't combine URLs from both `project.dependencies` and `tool.uv.sources` + Resolved 2 packages in [TIME] "###); + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "uv-public-pypackage" }, + ] + + [package.metadata] + requires-dist = [{ name = "uv-public-pypackage", git = "https://github.com/astral-test/uv-public-pypackage?tag=0.0.1" }] + + [[package]] + name = "uv-public-pypackage" + version = "0.1.0" + source = { git = "https://github.com/astral-test/uv-public-pypackage?tag=0.0.1#0dacfd662c64cb4ceb16e6cf65a157a8b715b979" } + "### + ); + }); + + // If we run with `--no-sources`, we should use the URL provided in `project.dependencies`. + uv_snapshot!(context.filters(), context.lock().arg("--no-sources"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + let lock = context.read("uv.lock"); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "uv-public-pypackage" }, + ] + + [package.metadata] + requires-dist = [{ name = "uv-public-pypackage", git = "https://github.com/astral-test/uv-public-pypackage?rev=0.0.2" }] + + [[package]] + name = "uv-public-pypackage" + version = "0.1.0" + source = { git = "https://github.com/astral-test/uv-public-pypackage?rev=0.0.2#b270df1a2fb5d012294e9aaf05e7e0bab1e6a389" } + "### + ); + }); + Ok(()) }