Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --extra to uv add and enable fine grained updates #4566

Merged
merged 4 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1771,6 +1771,10 @@ pub struct AddArgs {
#[arg(long)]
pub branch: Option<String>,

/// Extras to activate for the dependency; may be provided more than once.
#[arg(long)]
pub extra: Option<Vec<ExtraName>>,

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

Expand Down
94 changes: 61 additions & 33 deletions crates/uv-distribution/src/pyproject_mut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ use std::{fmt, mem};
use thiserror::Error;
use toml_edit::{Array, DocumentMut, Item, RawString, Table, TomlError, Value};

use pep508_rs::{PackageName, Requirement};
use pypi_types::VerbatimParsedUrl;
use pep508_rs::{PackageName, Requirement, VersionOrUrl};

use crate::pyproject::{PyProjectToml, Source};

Expand All @@ -27,6 +26,8 @@ pub enum Error {
MalformedDependencies,
#[error("Sources in `pyproject.toml` are malformed")]
MalformedSources,
#[error("Cannot perform ambiguous update; multiple entries with matching package names.")]
Ambiguous,
}

impl PyProjectTomlMut {
Expand All @@ -40,8 +41,8 @@ impl PyProjectTomlMut {
/// Adds a dependency to `project.dependencies`.
pub fn add_dependency(
&mut self,
req: &Requirement,
source: Option<&Source>,
req: Requirement,
source: Option<Source>,
) -> Result<(), Error> {
// Get or create `project.dependencies`.
let dependencies = self
Expand All @@ -55,7 +56,8 @@ impl PyProjectTomlMut {
.as_array_mut()
.ok_or(Error::MalformedDependencies)?;

add_dependency(req, dependencies);
let name = req.name.clone();
add_dependency(req, dependencies, source.is_some())?;

if let Some(source) = source {
// Get or create `tool.uv.sources`.
Expand All @@ -74,7 +76,7 @@ impl PyProjectTomlMut {
.as_table_mut()
.ok_or(Error::MalformedSources)?;

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

Ok(())
Expand All @@ -83,8 +85,8 @@ impl PyProjectTomlMut {
/// Adds a development dependency to `tool.uv.dev-dependencies`.
pub fn add_dev_dependency(
&mut self,
req: &Requirement,
source: Option<&Source>,
req: Requirement,
source: Option<Source>,
) -> Result<(), Error> {
// Get or create `tool.uv`.
let tool_uv = self
Expand All @@ -105,7 +107,8 @@ impl PyProjectTomlMut {
.as_array_mut()
.ok_or(Error::MalformedDependencies)?;

add_dependency(req, dev_dependencies);
let name = req.name.clone();
add_dependency(req, dev_dependencies, source.is_some())?;

if let Some(source) = source {
// Get or create `tool.uv.sources`.
Expand All @@ -115,7 +118,7 @@ impl PyProjectTomlMut {
.as_table_mut()
.ok_or(Error::MalformedSources)?;

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

Ok(())
Expand Down Expand Up @@ -194,19 +197,46 @@ fn implicit() -> Item {
}

/// Adds a dependency to the given `deps` array.
pub fn add_dependency(req: &Requirement, deps: &mut Array) {
pub fn add_dependency(req: Requirement, deps: &mut Array, has_source: bool) -> Result<(), Error> {
// Find matching dependencies.
let to_replace = find_dependencies(&req.name, deps);
if to_replace.is_empty() {
deps.push(req.to_string());
} else {
// Replace the first occurrence of the dependency and remove the rest.
deps.replace(to_replace[0], req.to_string());
for &i in to_replace[1..].iter().rev() {
deps.remove(i);
let mut to_replace = find_dependencies(&req.name, deps);
match to_replace.as_slice() {
[] => deps.push(req.to_string()),
[_] => {
let (i, mut old_req) = to_replace.remove(0);
update_requirement(&mut old_req, req, has_source);
deps.replace(i, old_req.to_string());
}
// Cannot perform ambiguous updates.
_ => return Err(Error::Ambiguous),
}
reformat_array_multiline(deps);
Ok(())
}

/// Update an existing requirement.
fn update_requirement(old: &mut Requirement, new: Requirement, has_source: bool) {
// Add any new extras.
old.extras.extend(new.extras);
old.extras.sort_unstable();
old.extras.dedup();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to sort?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so? This is updating pyproject.toml not the lockfile.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But dedup only deduplicates adjacent entries, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right, fixed. I wonder if extras should be a HashSet..


// Clear the requirement source if we are going to add to `tool.uv.sources`.
if has_source {
old.version_or_url = None;
}

// Update the source if a new one was specified.
match new.version_or_url {
None => {}
Some(VersionOrUrl::VersionSpecifier(specifier)) if specifier.is_empty() => {}
Some(version_or_url) => old.version_or_url = Some(version_or_url),
}

// Update the marker expression.
if let Some(marker) = new.marker {
old.marker = Some(marker);
}
}

/// Removes all occurrences of dependencies with the given name from the given `deps` array.
Expand All @@ -215,7 +245,7 @@ fn remove_dependency(req: &PackageName, deps: &mut Array) -> Vec<Requirement> {
let removed = find_dependencies(req, deps)
.into_iter()
.rev() // Reverse to preserve indices as we remove them.
.filter_map(|i| {
.filter_map(|(i, _)| {
deps.remove(i)
.as_str()
.and_then(|req| Requirement::from_str(req).ok())
Expand All @@ -229,32 +259,30 @@ fn remove_dependency(req: &PackageName, deps: &mut Array) -> Vec<Requirement> {
removed
}

// Returns a `Vec` containing the indices of all dependencies with the given name.
fn find_dependencies(name: &PackageName, deps: &Array) -> Vec<usize> {
// Returns a `Vec` containing the all dependencies with the given name, along with their positions
// in the array.
fn find_dependencies(name: &PackageName, deps: &Array) -> Vec<(usize, Requirement)> {
let mut to_replace = Vec::new();
for (i, dep) in deps.iter().enumerate() {
if dep
.as_str()
.and_then(try_parse_requirement)
.filter(|dep| dep.name == *name)
.is_some()
{
to_replace.push(i);
if let Some(req) = dep.as_str().and_then(try_parse_requirement) {
if req.name == *name {
to_replace.push((i, req));
}
}
}
to_replace
}

// Add a source to `tool.uv.sources`.
fn add_source(req: &Requirement, source: &Source, sources: &mut Table) -> Result<(), Error> {
fn add_source(req: &PackageName, source: &Source, sources: &mut Table) -> Result<(), Error> {
// Serialize as an inline table.
let mut doc = toml::to_string(source)
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)));
sources.insert(req.as_ref(), Item::Value(Value::InlineTable(table)));

Ok(())
}
Expand All @@ -265,7 +293,7 @@ impl fmt::Display for PyProjectTomlMut {
}
}

fn try_parse_requirement(req: &str) -> Option<Requirement<VerbatimParsedUrl>> {
fn try_parse_requirement(req: &str) -> Option<Requirement> {
Requirement::from_str(req).ok()
}

Expand Down
13 changes: 10 additions & 3 deletions crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use anyhow::{Context, Result};

use pep508_rs::ExtraName;
use uv_cache::Cache;
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{Concurrency, ExtrasSpecification, PreviewMode, SetupPyStrategy};
Expand Down Expand Up @@ -32,6 +33,7 @@ pub(crate) async fn add(
rev: Option<String>,
tag: Option<String>,
branch: Option<String>,
extras: Vec<ExtraName>,
package: Option<PackageName>,
python: Option<String>,
settings: ResolverInstallerSettings,
Expand Down Expand Up @@ -147,7 +149,12 @@ 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 {
for mut req in requirements {
// Add the specified extras.
req.extras.extend(extras.iter().cloned());
req.extras.sort_unstable();
req.extras.dedup();

let (req, source) = if raw_sources {
// Use the PEP 508 requirement directly.
(pep508_rs::Requirement::from(req), None)
Expand Down Expand Up @@ -180,9 +187,9 @@ pub(crate) async fn add(
};

if dev {
pyproject.add_dev_dependency(&req, source.as_ref())?;
pyproject.add_dev_dependency(req, source)?;
} else {
pyproject.add_dependency(&req, source.as_ref())?;
pyproject.add_dependency(req, source)?;
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,7 @@ async fn run() -> Result<ExitStatus> {
args.rev,
args.tag,
args.branch,
args.extras,
args.package,
args.python,
args.settings,
Expand Down
5 changes: 4 additions & 1 deletion crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::str::FromStr;

use distribution_types::IndexLocations;
use install_wheel_rs::linker::LinkMode;
use pep508_rs::RequirementOrigin;
use pep508_rs::{ExtraName, RequirementOrigin};
use pypi_types::Requirement;
use uv_cache::{CacheArgs, Refresh};
use uv_cli::options::{flag, installer_options, resolver_installer_options, resolver_options};
Expand Down Expand Up @@ -433,6 +433,7 @@ pub(crate) struct AddSettings {
pub(crate) requirements: Vec<RequirementsSource>,
pub(crate) dev: bool,
pub(crate) editable: Option<bool>,
pub(crate) extras: Vec<ExtraName>,
pub(crate) raw_sources: bool,
pub(crate) rev: Option<String>,
pub(crate) tag: Option<String>,
Expand All @@ -451,6 +452,7 @@ impl AddSettings {
requirements,
dev,
editable,
extra,
raw_sources,
rev,
tag,
Expand All @@ -477,6 +479,7 @@ impl AddSettings {
branch,
package,
python,
extras: extra.unwrap_or_default(),
refresh: Refresh::from(refresh),
settings: ResolverInstallerSettings::combine(
resolver_installer_options(installer, build),
Expand Down
Loading
Loading