Skip to content

Commit

Permalink
Allow adds
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Sep 17, 2024
1 parent 424ee43 commit 790ebf8
Show file tree
Hide file tree
Showing 3 changed files with 262 additions and 15 deletions.
7 changes: 7 additions & 0 deletions crates/uv-git/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ impl GitUrl {
self
}

/// Set the [`GitReference`] to use for this Git URL.
#[must_use]
pub fn with_reference(mut self, reference: GitReference) -> Self {
self.reference = reference;
self
}

/// Return the [`Url`] of the Git repository.
pub fn repository(&self) -> &Url {
&self.repository
Expand Down
104 changes: 90 additions & 14 deletions crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ use tracing::debug;

use cache_key::RepositoryUrl;
use distribution_types::UnresolvedRequirement;
use pep508_rs::{ExtraName, Requirement, VersionOrUrl};
use pypi_types::redact_git_credentials;
use pep508_rs::{ExtraName, Requirement, UnnamedRequirement, VersionOrUrl};
use pypi_types::{redact_git_credentials, ParsedUrl, RequirementSource, VerbatimParsedUrl};
use uv_auth::{store_credentials_from_url, Credentials};
use uv_cache::Cache;
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
Expand All @@ -21,7 +21,7 @@ use uv_configuration::{
use uv_dispatch::BuildDispatch;
use uv_distribution::DistributionDatabase;
use uv_fs::{Simplified, CWD};
use uv_git::GIT_STORE;
use uv_git::{GitReference, GIT_STORE};
use uv_normalize::PackageName;
use uv_python::{
EnvironmentPreference, Interpreter, PythonDownloads, PythonEnvironment, PythonInstallation,
Expand Down Expand Up @@ -317,17 +317,22 @@ pub(crate) async fn add(
// Resolve any unnamed requirements.
let requirements = {
// Partition the requirements into named and unnamed requirements.
let (mut requirements, unnamed): (Vec<_>, Vec<_>) =
requirements
.into_iter()
.partition_map(|spec| match spec.requirement {
UnresolvedRequirement::Named(requirement) => {
itertools::Either::Left(requirement)
}
UnresolvedRequirement::Unnamed(requirement) => {
itertools::Either::Right(requirement)
}
});
let (mut requirements, unnamed): (Vec<_>, Vec<_>) = requirements
.into_iter()
.map(|spec| {
augment_requirement(
spec.requirement,
rev.as_deref(),
tag.as_deref(),
branch.as_deref(),
)
})
.partition_map(|requirement| match requirement {
UnresolvedRequirement::Named(requirement) => itertools::Either::Left(requirement),
UnresolvedRequirement::Unnamed(requirement) => {
itertools::Either::Right(requirement)
}
});

// Resolve any unnamed requirements.
if !unnamed.is_empty() {
Expand Down Expand Up @@ -766,6 +771,77 @@ async fn lock_and_sync(
Ok(())
}

/// Augment a user-provided requirement by attaching any specification data that was provided
/// separately from the requirement itself (e.g., `--branch main`).
fn augment_requirement(
requirement: UnresolvedRequirement,
rev: Option<&str>,
tag: Option<&str>,
branch: Option<&str>,
) -> UnresolvedRequirement {
match requirement {
UnresolvedRequirement::Named(requirement) => {
UnresolvedRequirement::Named(pypi_types::Requirement {
source: match requirement.source {
RequirementSource::Git {
repository,
reference,
precise,
subdirectory,
url,
} => {
let reference = if let Some(rev) = rev {
GitReference::from_rev(rev.to_string())
} else if let Some(tag) = tag {
GitReference::Tag(tag.to_string())
} else if let Some(branch) = branch {
GitReference::Branch(branch.to_string())
} else {
reference
};
RequirementSource::Git {
repository,
reference,
precise,
subdirectory,
url,
}
}
_ => requirement.source,
},
..requirement
})
}
UnresolvedRequirement::Unnamed(requirement) => {
UnresolvedRequirement::Unnamed(UnnamedRequirement {
url: match requirement.url.parsed_url {
ParsedUrl::Git(mut git) => {
if let Some(rev) = rev {
git.url = git
.url
.with_reference(GitReference::from_rev(rev.to_string()));
}
if let Some(tag) = tag {
git.url = git.url.with_reference(GitReference::Tag(tag.to_string()));
}
if let Some(branch) = branch {
git.url = git
.url
.with_reference(GitReference::Branch(branch.to_string()));
}
VerbatimParsedUrl {
parsed_url: ParsedUrl::Git(git),
verbatim: requirement.url.verbatim,
}
}
_ => requirement.url,
},
..requirement
})
}
}
}

/// Resolves the source for a requirement and processes it into a PEP 508 compliant format.
fn resolve_requirement(
requirement: pypi_types::Requirement,
Expand Down
166 changes: 165 additions & 1 deletion crates/uv/tests/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,40 @@ fn add_git_error() -> Result<()> {
Ok(())
}

#[test]
#[cfg(feature = "git")]
fn add_git_branch() -> 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 = []
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#})?;

uv_snapshot!(context.filters(), context.add().arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage").arg("--branch").arg("test-branch"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ project==0.1.0 (from file://[TEMP_DIR]/)
+ uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-public-pypackage@0dacfd662c64cb4ceb16e6cf65a157a8b715b979)
"###);

Ok(())
}

/// Add a Git requirement using the `--raw-sources` API.
#[test]
#[cfg(feature = "git")]
Expand Down Expand Up @@ -1713,6 +1747,12 @@ fn add_workspace_editable() -> Result<()> {

let workspace = context.temp_dir.child("pyproject.toml");
workspace.write_str(indoc! {r#"
[project]
name = "parent"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
[tool.uv.workspace]
members = ["child1", "child2"]
"#})?;
Expand Down Expand Up @@ -1753,7 +1793,7 @@ fn add_workspace_editable() -> Result<()> {
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Resolved 3 packages in [TIME]
Prepared 2 packages in [TIME]
Installed 2 packages in [TIME]
+ child1==0.1.0 (from file://[TEMP_DIR]/child1)
Expand Down Expand Up @@ -1803,6 +1843,7 @@ fn add_workspace_editable() -> Result<()> {
members = [
"child1",
"child2",
"parent",
]
[[package]]
Expand All @@ -1820,6 +1861,11 @@ fn add_workspace_editable() -> Result<()> {
name = "child2"
version = "0.1.0"
source = { editable = "child2" }
[[package]]
name = "parent"
version = "0.1.0"
source = { virtual = "." }
"###
);
});
Expand All @@ -1837,6 +1883,124 @@ fn add_workspace_editable() -> Result<()> {
Ok(())
}

/// Add a workspace dependency via its path.
#[test]
fn add_workspace_path() -> Result<()> {
let context = TestContext::new("3.12");

let workspace = context.temp_dir.child("pyproject.toml");
workspace.write_str(indoc! {r#"
[project]
name = "parent"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
[tool.uv.workspace]
members = ["child"]
"#})?;

let pyproject_toml = context.temp_dir.child("child/pyproject.toml");
pyproject_toml.write_str(indoc! {r#"
[project]
name = "child"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
"#})?;

uv_snapshot!(context.filters(), context.add().arg("./child"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ child==0.1.0 (from file://[TEMP_DIR]/child)
"###);

let pyproject_toml = fs_err::read_to_string(context.temp_dir.child("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.workspace]
members = ["child"]
[tool.uv.sources]
child = { workspace = true }
"###
);
});

// `uv add` implies a full lock and sync, including development dependencies.
let lock = fs_err::read_to_string(context.temp_dir.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"
[manifest]
members = [
"child",
"parent",
]
[[package]]
name = "child"
version = "0.1.0"
source = { editable = "child" }
[[package]]
name = "parent"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "child" },
]
[package.metadata]
requires-dist = [{ name = "child", editable = "child" }]
"###
);
});

// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Audited 1 package in [TIME]
"###);

Ok(())
}

/// Add a path dependency.
#[test]
fn add_path() -> Result<()> {
Expand Down

0 comments on commit 790ebf8

Please sign in to comment.