Skip to content

Commit

Permalink
Opt-out tool.uv.sources support for uv add (#4406)
Browse files Browse the repository at this point in the history
## Summary

After this change, `uv add` will try to use `tool.uv.sources` for all
source requirements. If a source cannot be resolved, i.e. an ambiguous
Git reference is provided, it will error. Git references can be
specified with the `--tag`, `--branch`, or `--rev` arguments. Editables
are also supported with `--editable`.

Users can opt-out of `tool.uv.sources` support with the `--raw` flag,
which will force uv to use `project.dependencies`.

Part of #3959.
  • Loading branch information
ibraheemdev authored Jun 19, 2024
1 parent 3c5b136 commit 7b72b55
Show file tree
Hide file tree
Showing 8 changed files with 484 additions and 40 deletions.
9 changes: 9 additions & 0 deletions crates/pep508-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,15 @@ pub struct Requirement<T: Pep508Url = VerbatimUrl> {
pub origin: Option<RequirementOrigin>,
}

impl<T: Pep508Url> Requirement<T> {
/// Removes the URL specifier from this requirement.
pub fn clear_url(&mut self) {
if matches!(self.version_or_url, Some(VersionOrUrl::Url(_))) {
self.version_or_url = None;
}
}
}

impl<T: Pep508Url + Display> Display for Requirement<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name)?;
Expand Down
90 changes: 88 additions & 2 deletions crates/uv-distribution/src/pyproject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@
//!
//! Then lowers them into a dependency specification.
use std::collections::BTreeMap;
use std::ops::Deref;
use std::{collections::BTreeMap, mem};

use glob::Pattern;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use url::Url;

use pep440_rs::VersionSpecifiers;
use pypi_types::VerbatimParsedUrl;
use pypi_types::{RequirementSource, VerbatimParsedUrl};
use uv_git::GitReference;
use uv_normalize::{ExtraName, PackageName};

/// A `pyproject.toml` as specified in PEP 517.
Expand Down Expand Up @@ -182,6 +184,90 @@ pub enum Source {
},
}

#[derive(Error, Debug)]
pub enum SourceError {
#[error("Cannot resolve git reference `{0}`.")]
UnresolvedReference(String),
#[error("Workspace dependency must be a local path.")]
InvalidWorkspaceRequirement,
}

impl Source {
pub fn from_requirement(
source: RequirementSource,
workspace: bool,
editable: Option<bool>,
rev: Option<String>,
tag: Option<String>,
branch: Option<String>,
) -> Result<Option<Source>, SourceError> {
if workspace {
match source {
RequirementSource::Registry { .. } | RequirementSource::Directory { .. } => {}
_ => return Err(SourceError::InvalidWorkspaceRequirement),
}

return Ok(Some(Source::Workspace {
editable,
workspace: true,
}));
}

let source = match source {
RequirementSource::Registry { .. } => return Ok(None),
RequirementSource::Path { lock_path, .. } => Source::Path {
editable,
path: lock_path.to_string_lossy().into_owned(),
},
RequirementSource::Directory { lock_path, .. } => Source::Path {
editable,
path: lock_path.to_string_lossy().into_owned(),
},
RequirementSource::Url {
subdirectory, url, ..
} => Source::Url {
url: url.to_url(),
subdirectory: subdirectory.map(|path| path.to_string_lossy().into_owned()),
},
RequirementSource::Git {
repository,
mut reference,
subdirectory,
..
} => {
// We can only resolve a full commit hash from a pep508 URL, everything else is ambiguous.
let rev = match reference {
GitReference::FullCommit(ref mut rev) => Some(mem::take(rev)),
_ => None,
}
// Give precedence to an explicit argument.
.or(rev);

// Error if the user tried to specify a reference but didn't disambiguate.
if reference != GitReference::DefaultBranch
&& rev.is_none()
&& tag.is_none()
&& branch.is_none()
{
return Err(SourceError::UnresolvedReference(
reference.as_str().unwrap().to_owned(),
));
}

Source::Git {
rev,
tag,
branch,
git: repository,
subdirectory: subdirectory.map(|path| path.to_string_lossy().into_owned()),
}
}
};

Ok(Some(source))
}
}

/// <https://github.com/serde-rs/serde/issues/1316#issue-332908452>
mod serde_from_and_to_string {
use std::fmt::Display;
Expand Down
36 changes: 17 additions & 19 deletions crates/uv-distribution/src/pyproject_mut.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::fmt;
use std::str::FromStr;
use std::{fmt, mem};

use thiserror::Error;
use toml_edit::{Array, DocumentMut, InlineTable, Item, RawString, Table, TomlError, Value};
use toml_edit::{Array, DocumentMut, Item, RawString, Table, TomlError, Value};

use pep508_rs::{PackageName, Requirement};
use pypi_types::VerbatimParsedUrl;
Expand All @@ -21,6 +21,8 @@ pub struct PyProjectTomlMut {
pub enum Error {
#[error("Failed to parse `pyproject.toml`")]
Parse(#[from] Box<TomlError>),
#[error("Failed to serialize `pyproject.toml`")]
Serialize(#[from] Box<toml::ser::Error>),
#[error("Dependencies in `pyproject.toml` are malformed")]
MalformedDependencies,
#[error("Sources in `pyproject.toml` are malformed")]
Expand Down Expand Up @@ -72,7 +74,7 @@ impl PyProjectTomlMut {
.as_table_mut()
.ok_or(Error::MalformedSources)?;

add_source(req, source, sources);
add_source(req, source, sources)?;
}

Ok(())
Expand Down Expand Up @@ -113,7 +115,7 @@ impl PyProjectTomlMut {
.as_table_mut()
.ok_or(Error::MalformedSources)?;

add_source(req, source, sources);
add_source(req, source, sources)?;
}

Ok(())
Expand Down Expand Up @@ -244,21 +246,17 @@ fn find_dependencies(name: &PackageName, deps: &Array) -> Vec<usize> {
}

// Add a source to `tool.uv.sources`.
fn add_source(req: &Requirement, source: &Source, sources: &mut Table) {
match source {
Source::Workspace {
workspace,
editable,
} => {
let mut value = InlineTable::new();
value.insert("workspace", Value::from(*workspace));
if let Some(editable) = editable {
value.insert("editable", Value::from(*editable));
}
sources.insert(req.name.as_ref(), Item::Value(Value::InlineTable(value)));
}
_ => unimplemented!(),
}
fn add_source(req: &Requirement, source: &Source, sources: &mut Table) -> Result<(), Error> {
// Serialize as an inline table.
let mut doc = toml::to_string(source)
.map_err(Box::new)?
.parse::<DocumentMut>()
.unwrap();
let table = mem::take(doc.as_table_mut()).into_inline_table();

sources.insert(req.name.as_ref(), Item::Value(Value::InlineTable(table)));

Ok(())
}

impl fmt::Display for PyProjectTomlMut {
Expand Down
22 changes: 22 additions & 0 deletions crates/uv/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1638,6 +1638,28 @@ pub(crate) struct AddArgs {
#[arg(long)]
pub(crate) workspace: bool,

/// Add the requirements as editables.
#[arg(long, default_missing_value = "true", num_args(0..=1))]
pub(crate) editable: Option<bool>,

/// Add source requirements to the `project.dependencies` section of the `pyproject.toml`.
///
/// Without this flag uv will try to use `tool.uv.sources` for any sources.
#[arg(long)]
pub(crate) raw: bool,

/// Specific commit to use when adding from Git.
#[arg(long)]
pub(crate) rev: Option<String>,

/// Tag to use when adding from git.
#[arg(long)]
pub(crate) tag: Option<String>,

/// Branch to use when adding from git.
#[arg(long)]
pub(crate) branch: Option<String>,

#[command(flatten)]
pub(crate) installer: ResolverInstallerArgs,

Expand Down
43 changes: 34 additions & 9 deletions crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use anyhow::Result;
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
use uv_dispatch::BuildDispatch;
use uv_distribution::pyproject::Source;
use uv_distribution::pyproject::{Source, SourceError};
use uv_distribution::pyproject_mut::PyProjectTomlMut;
use uv_git::GitResolver;
use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification};
Expand All @@ -22,11 +22,16 @@ use crate::printer::Printer;
use crate::settings::ResolverInstallerSettings;

/// Add one or more packages to the project requirements.
#[allow(clippy::too_many_arguments)]
#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
pub(crate) async fn add(
requirements: Vec<RequirementsSource>,
workspace: bool,
dev: bool,
editable: Option<bool>,
raw: bool,
rev: Option<String>,
tag: Option<String>,
branch: Option<String>,
python: Option<String>,
settings: ResolverInstallerSettings,
preview: PreviewMode,
Expand Down Expand Up @@ -135,14 +140,34 @@ pub(crate) async fn add(

// Add the requirements to the `pyproject.toml`.
let mut pyproject = PyProjectTomlMut::from_toml(project.current_project().pyproject_toml())?;
for req in requirements.into_iter().map(pep508_rs::Requirement::from) {
let source = if workspace {
Some(Source::Workspace {
workspace: true,
editable: None,
})
for req in requirements {
let (req, source) = if raw {
// Use the PEP 508 requirement directly.
(pep508_rs::Requirement::from(req), None)
} else {
None
// Otherwise, try to construct the source.
let result = Source::from_requirement(
req.source.clone(),
workspace,
editable,
rev.clone(),
tag.clone(),
branch.clone(),
);

let source = match result {
Ok(source) => source,
Err(SourceError::UnresolvedReference(rev)) => {
anyhow::bail!("Cannot resolve Git reference `{rev}` for requirement `{}`. Specify the reference with one of `--tag`, `--branch`, or `--rev`, or use the `--raw` flag.", req.name)
}
Err(err) => return Err(err.into()),
};

// Ignore the PEP 508 source.
let mut req = pep508_rs::Requirement::from(req);
req.clear_url();

(req, source)
};

if dev {
Expand Down
5 changes: 5 additions & 0 deletions crates/uv/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,11 @@ async fn run() -> Result<ExitStatus> {
args.requirements,
args.workspace,
args.dev,
args.editable,
args.raw,
args.rev,
args.tag,
args.branch,
args.python,
args.settings,
globals.preview,
Expand Down
17 changes: 16 additions & 1 deletion crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -368,8 +368,13 @@ impl LockSettings {
#[derive(Debug, Clone)]
pub(crate) struct AddSettings {
pub(crate) requirements: Vec<RequirementsSource>,
pub(crate) workspace: bool,
pub(crate) dev: bool,
pub(crate) workspace: bool,
pub(crate) editable: Option<bool>,
pub(crate) raw: bool,
pub(crate) rev: Option<String>,
pub(crate) tag: Option<String>,
pub(crate) branch: Option<String>,
pub(crate) python: Option<String>,
pub(crate) refresh: Refresh,
pub(crate) settings: ResolverInstallerSettings,
Expand All @@ -383,6 +388,11 @@ impl AddSettings {
requirements,
dev,
workspace,
editable,
raw,
rev,
tag,
branch,
installer,
build,
refresh,
Expand All @@ -398,6 +408,11 @@ impl AddSettings {
requirements,
workspace,
dev,
editable,
raw,
rev,
tag,
branch,
python,
refresh: Refresh::from(refresh),
settings: ResolverInstallerSettings::combine(
Expand Down
Loading

0 comments on commit 7b72b55

Please sign in to comment.