From 3336cee8bb2717fa027c756d248a7394f0cd9d45 Mon Sep 17 00:00:00 2001 From: Max Mynter Date: Tue, 15 Apr 2025 13:20:50 +0200 Subject: [PATCH 1/6] Fallback to known Git source for same no-Git source with Git reference When adding a package with Git reference options (--rev, --tag, --branch) that already has a Git source defined, use the existing Git URL with the new reference instead of reporting an error. --- crates/uv-workspace/src/pyproject.rs | 33 +++++++++++++++++++++++++-- crates/uv/src/commands/project/add.rs | 12 +++++++++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 6b2cc9930f173..18a468011501f 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -1378,9 +1378,38 @@ impl Source { tag: Option, branch: Option, root: &Path, + existing_sources: Option<&ToolUvSources>, ) -> Result, SourceError> { - // If we resolved to a non-Git source, and the user specified a Git reference, error. - if !matches!(source, RequirementSource::Git { .. }) { + // If the user specified a Git reference for a non-Git source, try existing Git sources before erroring. + if !matches!(source, RequirementSource::Git { .. }) + && (branch.is_some() || tag.is_some() || rev.is_some()) + { + if let Some(sources) = existing_sources { + if let Some(package_sources) = sources.inner().get(name) { + for existing_source in package_sources.iter() { + if let Source::Git { + git, + subdirectory, + marker, + extra, + group, + .. + } = existing_source + { + return Ok(Some(Source::Git { + git: git.clone(), + subdirectory: subdirectory.clone(), + rev, + tag, + branch, + marker: *marker, + extra: extra.clone(), + group: group.clone(), + })); + } + } + } + } if let Some(rev) = rev { return Err(SourceError::UnusedRev(name.to_string(), rev)); } diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 8b96364e53975..f611115aeaeba 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -40,7 +40,7 @@ use uv_scripts::{Pep723ItemRef, Pep723Metadata, Pep723Script}; use uv_settings::PythonInstallMirrors; use uv_types::{BuildIsolation, HashStrategy}; use uv_warnings::warn_user_once; -use uv_workspace::pyproject::{DependencyType, Source, SourceError}; +use uv_workspace::pyproject::{DependencyType, Source, SourceError, ToolUvSources}; use uv_workspace::pyproject_mut::{ArrayEdit, DependencyTarget, PyProjectTomlMut}; use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace, WorkspaceCache}; @@ -463,9 +463,16 @@ pub(crate) async fn add( tag.clone(), branch.clone(), script_dir, + None, )? } AddTarget::Project(ref project, _) => { + let existing_sources = project + .pyproject_toml() + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.sources.as_ref()); let workspace = project .workspace() .packages() @@ -479,6 +486,7 @@ pub(crate) async fn add( tag.clone(), branch.clone(), project.root(), + existing_sources, )? } }; @@ -1010,6 +1018,7 @@ fn resolve_requirement( tag: Option, branch: Option, root: &Path, + existing_sources: Option<&ToolUvSources>, ) -> Result<(uv_pep508::Requirement, Option), anyhow::Error> { let result = Source::from_requirement( &requirement.name, @@ -1021,6 +1030,7 @@ fn resolve_requirement( tag, branch, root, + existing_sources, ); let source = match result { From 56a0a67f31de8de10a2dcdc7a81e4be036603fd0 Mon Sep 17 00:00:00 2001 From: Max Mynter Date: Tue, 15 Apr 2025 14:41:40 +0200 Subject: [PATCH 2/6] Extend Git reference fallback to script dependencies --- crates/uv-workspace/src/pyproject.rs | 4 ++-- crates/uv/src/commands/project/add.rs | 11 +++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 18a468011501f..d8c4ea727ef4e 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -1378,14 +1378,14 @@ impl Source { tag: Option, branch: Option, root: &Path, - existing_sources: Option<&ToolUvSources>, + existing_sources: Option<&BTreeMap>, ) -> Result, SourceError> { // If the user specified a Git reference for a non-Git source, try existing Git sources before erroring. if !matches!(source, RequirementSource::Git { .. }) && (branch.is_some() || tag.is_some() || rev.is_some()) { if let Some(sources) = existing_sources { - if let Some(package_sources) = sources.inner().get(name) { + if let Some(package_sources) = sources.get(name) { for existing_source in package_sources.iter() { if let Source::Git { git, diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index f611115aeaeba..41601b1ede868 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -40,7 +40,7 @@ use uv_scripts::{Pep723ItemRef, Pep723Metadata, Pep723Script}; use uv_settings::PythonInstallMirrors; use uv_types::{BuildIsolation, HashStrategy}; use uv_warnings::warn_user_once; -use uv_workspace::pyproject::{DependencyType, Source, SourceError, ToolUvSources}; +use uv_workspace::pyproject::{DependencyType, Source, SourceError, Sources, ToolUvSources}; use uv_workspace::pyproject_mut::{ArrayEdit, DependencyTarget, PyProjectTomlMut}; use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace, WorkspaceCache}; @@ -454,6 +454,8 @@ pub(crate) async fn add( AddTarget::Script(ref script, _) => { let script_path = std::path::absolute(&script.path)?; let script_dir = script_path.parent().expect("script path has no parent"); + + let existing_sources = Some(script.sources()); resolve_requirement( requirement, false, @@ -463,7 +465,7 @@ pub(crate) async fn add( tag.clone(), branch.clone(), script_dir, - None, + existing_sources, )? } AddTarget::Project(ref project, _) => { @@ -472,7 +474,8 @@ pub(crate) async fn add( .tool .as_ref() .and_then(|tool| tool.uv.as_ref()) - .and_then(|uv| uv.sources.as_ref()); + .and_then(|uv| uv.sources.as_ref()) + .map(ToolUvSources::inner); let workspace = project .workspace() .packages() @@ -1018,7 +1021,7 @@ fn resolve_requirement( tag: Option, branch: Option, root: &Path, - existing_sources: Option<&ToolUvSources>, + existing_sources: Option<&BTreeMap>, ) -> Result<(uv_pep508::Requirement, Option), anyhow::Error> { let result = Source::from_requirement( &requirement.name, From 9c0fdfc914d432e6205036d4000392315ed6ec9d Mon Sep 17 00:00:00 2001 From: Max Mynter Date: Wed, 16 Apr 2025 18:51:27 +0200 Subject: [PATCH 3/6] (tests) Resolve known Git url for pkg name with Git reference ref --- crates/uv/tests/it/edit.rs | 71 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 0c6ef66b5cb20..0287d7dee0827 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -3062,6 +3062,77 @@ fn add_non_normalized_source() -> Result<()> { Ok(()) } +/// Test updating an existing Git reference with branch/tag/rev options without respecifying URL +#[test] +#[cfg(feature = "git")] +fn add_update_git_reference() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + "#})?; + + uv_snapshot!(context.filters(), context.add().arg("https://github.com/astral-test/uv-public-pypackage.git"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage.git@b270df1a2fb5d012294e9aaf05e7e0bab1e6a389) + "); + + uv_snapshot!(context.filters(), context.add().arg("uv-public-pypackage").arg("--tag=0.0.1"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + - uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage.git@b270df1a2fb5d012294e9aaf05e7e0bab1e6a389) + + uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage.git@0dacfd662c64cb4ceb16e6cf65a157a8b715b979) + "); + + uv_snapshot!(context.filters(), context.add().arg("uv-public-pypackage").arg("--branch=main"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + - uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage.git@0dacfd662c64cb4ceb16e6cf65a157a8b715b979) + + uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage.git@b270df1a2fb5d012294e9aaf05e7e0bab1e6a389) + "); + + uv_snapshot!(context.filters(), context.add().arg("uv-public-pypackage").arg("--rev=2005223fcad0e2c06daf2e14b93b790604868e1e"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + - uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage.git@b270df1a2fb5d012294e9aaf05e7e0bab1e6a389) + + uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage.git@2005223fcad0e2c06daf2e14b93b790604868e1e) + "); + + Ok(()) +} + /// If a source defined in `tool.uv.sources` but its name is not normalized, `uv remove` should /// remove the source. #[test] From b8a15a1d23fdb164e216363d1dcda1f0f1038d6e Mon Sep 17 00:00:00 2001 From: Max Mynter Date: Wed, 16 Apr 2025 20:38:52 +0200 Subject: [PATCH 4/6] (tests) Add test for Git source update by name in script --- crates/uv/tests/it/edit.rs | 91 +++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 0287d7dee0827..8713e27af27cd 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -3063,9 +3063,10 @@ fn add_non_normalized_source() -> Result<()> { } /// Test updating an existing Git reference with branch/tag/rev options without respecifying URL +/// in project #[test] #[cfg(feature = "git")] -fn add_update_git_reference() -> Result<()> { +fn add_update_git_reference_project() -> Result<()> { let context = TestContext::new("3.12"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -3133,6 +3134,94 @@ fn add_update_git_reference() -> Result<()> { Ok(()) } +/// Test updating an existing Git reference with branch/tag/rev options without respecifying URL +/// in script +#[test] +#[cfg(feature = "git")] +fn add_update_git_reference_script() -> Result<()> { + let context = TestContext::new("3.12"); + let script = context.temp_dir.child("script.py"); + script.write_str(indoc! { + r#" + # /// script + # requires-python = ">=3.11" + # dependencies = [ ] + # /// + + import time + time.sleep(5) + "# + })?; + + uv_snapshot!(context.filters(), context.add().arg("--script=script.py").arg("https://github.com/astral-test/uv-public-pypackage.git"), + @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Updated `script.py` + "### + ); + + let script_content = context.read("script.py"); + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + script_content, @r###" + # /// script + # requires-python = ">=3.11" + # dependencies = [ + # "uv-public-pypackage", + # ] + # + # [tool.uv.sources] + # uv-public-pypackage = { git = "https://github.com/astral-test/uv-public-pypackage.git" } + # /// + + import time + time.sleep(5) + "### + ); + }); + + uv_snapshot!(context.filters(), context.add().arg("--script=script.py").arg("uv-public-pypackage").arg("--branch=test-branch"), + @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Updated `script.py` + "### + ); + + let script_content = context.read("script.py"); + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + script_content, @r###" + # /// script + # requires-python = ">=3.11" + # dependencies = [ + # "uv-public-pypackage", + # ] + # + # [tool.uv.sources] + # uv-public-pypackage = { git = "https://github.com/astral-test/uv-public-pypackage.git", branch = "test-branch" } + # /// + + import time + time.sleep(5) + "### + ); + }); + + Ok(()) +} + /// If a source defined in `tool.uv.sources` but its name is not normalized, `uv remove` should /// remove the source. #[test] From 62e13576f6b345aa9a873cbde602747bf3247676 Mon Sep 17 00:00:00 2001 From: Max Mynter Date: Wed, 16 Apr 2025 23:17:14 +0200 Subject: [PATCH 5/6] Empty commit to trigger CI; retry bc. of timeout From edfdd5b62d916cdd3c92f2cf306ca4454be507af Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 20 Apr 2025 22:04:21 -0400 Subject: [PATCH 6/6] Minor tweaks --- crates/uv/tests/it/edit.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index a458231165b07..b41c2cdac817c 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -3064,8 +3064,8 @@ fn add_non_normalized_source() -> Result<()> { Ok(()) } -/// Test updating an existing Git reference with branch/tag/rev options without respecifying URL -/// in project +/// Test updating an existing Git reference with branch/tag/rev options without re- specifying the +/// URL. #[test] #[cfg(feature = "git")] fn add_update_git_reference_project() -> Result<()> { @@ -3136,8 +3136,8 @@ fn add_update_git_reference_project() -> Result<()> { Ok(()) } -/// Test updating an existing Git reference with branch/tag/rev options without respecifying URL -/// in script +/// Test updating an existing Git reference with branch/tag/rev options without re-specifying the +/// URL in a script. #[test] #[cfg(feature = "git")] fn add_update_git_reference_script() -> Result<()> {