From 3f15f2d922b74a5d8b6c11c0eaa806c50eb9a443 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 27 Aug 2024 10:02:08 -0400 Subject: [PATCH] Use relative paths by default in `uv add` (#6686) ## Summary Closes https://github.com/astral-sh/uv/issues/6684. --- crates/uv-workspace/src/pyproject.rs | 21 +++-- crates/uv/src/commands/project/add.rs | 6 +- crates/uv/tests/edit.rs | 107 +++++++++++++++++++++++++- 3 files changed, 126 insertions(+), 8 deletions(-) diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 2e4a00cfe418..c2eaee29652f 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -7,6 +7,7 @@ //! Then lowers them into a dependency specification. use std::ops::Deref; +use std::path::{Path, PathBuf}; use std::{collections::BTreeMap, mem}; use glob::Pattern; @@ -16,6 +17,7 @@ use url::Url; use pep440_rs::VersionSpecifiers; use pypi_types::{RequirementSource, SupportedEnvironments, VerbatimParsedUrl}; +use uv_fs::relative_to; use uv_git::GitReference; use uv_macros::OptionsMetadata; use uv_normalize::{ExtraName, PackageName}; @@ -341,6 +343,10 @@ pub enum SourceError { UnusedTag(String, String), #[error("`{0}` did not resolve to a Git repository, but a Git reference (`--branch {1}`) was provided.")] UnusedBranch(String, String), + #[error("Failed to resolve absolute path")] + Absolute(#[from] std::io::Error), + #[error("Path contains invalid characters: `{}`", _0.display())] + NonUtf8Path(PathBuf), } impl Source { @@ -352,6 +358,7 @@ impl Source { rev: Option, tag: Option, branch: Option, + root: &Path, ) -> Result, SourceError> { // If we resolved to a non-Git source, and the user specified a Git reference, error. if !matches!(source, RequirementSource::Git { .. }) { @@ -386,13 +393,15 @@ impl Source { let source = match source { RequirementSource::Registry { .. } => return Ok(None), - RequirementSource::Path { install_path, .. } => Source::Path { + RequirementSource::Path { install_path, .. } + | RequirementSource::Directory { install_path, .. } => Source::Path { editable, - path: install_path.to_string_lossy().into_owned(), - }, - RequirementSource::Directory { install_path, .. } => Source::Path { - editable, - path: install_path.to_string_lossy().into_owned(), + path: relative_to(&install_path, root) + .or_else(|_| std::path::absolute(&install_path)) + .map_err(SourceError::Absolute)? + .to_str() + .ok_or_else(|| SourceError::NonUtf8Path(install_path))? + .to_string(), }, RequirementSource::Url { subdirectory, url, .. diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 7895645dd724..9c97d42d860a 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -338,13 +338,14 @@ pub(crate) async fn add( Target::Script(_, _) | Target::Project(_, _) if raw_sources => { (pep508_rs::Requirement::from(requirement), None) } - Target::Script(_, _) => resolve_requirement( + Target::Script(ref script, _) => resolve_requirement( requirement, false, editable, rev.clone(), tag.clone(), branch.clone(), + &script.path, )?, Target::Project(ref project, _) => { let workspace = project @@ -358,6 +359,7 @@ pub(crate) async fn add( rev.clone(), tag.clone(), branch.clone(), + project.root(), )? } }; @@ -681,6 +683,7 @@ fn resolve_requirement( rev: Option, tag: Option, branch: Option, + root: &Path, ) -> Result<(Requirement, Option), anyhow::Error> { let result = Source::from_requirement( &requirement.name, @@ -690,6 +693,7 @@ fn resolve_requirement( rev, tag, branch, + root, ); let source = match result { diff --git a/crates/uv/tests/edit.rs b/crates/uv/tests/edit.rs index 83606f8c0cd9..c8425aa140c2 100644 --- a/crates/uv/tests/edit.rs +++ b/crates/uv/tests/edit.rs @@ -1626,6 +1626,111 @@ fn add_workspace_editable() -> Result<()> { Ok(()) } +/// Add a path dependency. +#[test] +fn add_path() -> Result<()> { + let context = TestContext::new("3.12"); + + let workspace = context.temp_dir.child("workspace"); + workspace.child("pyproject.toml").write_str(indoc! {r#" + [project] + name = "parent" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + "#})?; + + let child = workspace.child("child"); + child.child("pyproject.toml").write_str(indoc! {r#" + [project] + name = "child" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [] + "#})?; + + uv_snapshot!(context.filters(), context.add(&["./child"]).current_dir(workspace.path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using Python 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtualenv at: .venv + Resolved 2 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + child==0.1.0 (from file://[TEMP_DIR]/workspace/child) + + parent==0.1.0 (from file://[TEMP_DIR]/workspace) + "###); + + let pyproject_toml = fs_err::read_to_string(workspace.join("pyproject.toml"))?; + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + pyproject_toml, @r###" + [project] + name = "parent" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "child", + ] + + [tool.uv.sources] + child = { path = "child" } + "### + ); + }); + + // `uv add` implies a full lock and sync, including development dependencies. + let lock = fs_err::read_to_string(workspace.join("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 = "child" + version = "0.1.0" + source = { directory = "child" } + + [[package]] + name = "parent" + version = "0.1.0" + source = { editable = "." } + dependencies = [ + { name = "child" }, + ] + + [package.metadata] + requires-dist = [{ name = "child", directory = "child" }] + "### + ); + }); + + // Install from the lockfile. + uv_snapshot!(context.filters(), context.sync().arg("--frozen").current_dir(workspace.path()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Audited 2 packages in [TIME] + "###); + + Ok(()) +} + /// Update a requirement, modifying the source and extras. #[test] #[cfg(feature = "git")] @@ -3868,7 +3973,7 @@ fn add_git_to_script() -> Result<()> { Ok(()) } -// Revert changes to pyproject.toml if add fails +/// Revert changes to a `pyproject.toml` the `add` fails. #[test] fn fail_to_add_revert_project() -> Result<()> { let context = TestContext::new("3.12");