Skip to content

Add --workspace option to uv add #4362

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

Merged
merged 3 commits into from
Jun 18, 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
196 changes: 163 additions & 33 deletions crates/uv-distribution/src/pyproject_mut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ use std::fmt;
use std::str::FromStr;

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

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

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

/// Raw and mutable representation of a `pyproject.toml`.
///
Expand All @@ -23,6 +23,8 @@ pub enum Error {
Parse(#[from] Box<TomlError>),
#[error("Dependencies in `pyproject.toml` are malformed")]
MalformedDependencies,
#[error("Sources in `pyproject.toml` are malformed")]
MalformedSources,
}

impl PyProjectTomlMut {
Expand All @@ -34,47 +36,165 @@ impl PyProjectTomlMut {
}

/// Adds a dependency to `project.dependencies`.
pub fn add_dependency(&mut self, req: &Requirement) -> Result<(), Error> {
add_dependency(req, &mut self.doc["project"]["dependencies"])
pub fn add_dependency(
&mut self,
req: &Requirement,
source: Option<&Source>,
) -> Result<(), Error> {
// Get or create `project.dependencies`.
let dependencies = self
Copy link
Member Author

@ibraheemdev ibraheemdev Jun 17, 2024

Choose a reason for hiding this comment

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

toml_edit does some implicit conversions/insertion when indexing directly on an Item which makes this form a lot better.. but very verbose. Maybe an extension trait could help for some of the common conversions.

.doc
.entry("project")
.or_insert(Item::Table(Table::new()))
.as_table_mut()
.ok_or(Error::MalformedDependencies)?
.entry("dependencies")
.or_insert(Item::Value(Value::Array(Array::new())))
.as_array_mut()
.ok_or(Error::MalformedDependencies)?;

add_dependency(req, dependencies);

if let Some(source) = source {
// Get or create `tool.uv.sources`.
let sources = self
.doc
.entry("tool")
.or_insert(implicit())
.as_table_mut()
.ok_or(Error::MalformedSources)?
.entry("uv")
.or_insert(implicit())
.as_table_mut()
.ok_or(Error::MalformedSources)?
.entry("sources")
.or_insert(Item::Table(Table::new()))
.as_table_mut()
.ok_or(Error::MalformedSources)?;

add_source(req, source, sources);
}

Ok(())
}

/// Adds a development dependency to `tool.uv.dev-dependencies`.
pub fn add_dev_dependency(&mut self, req: &Requirement) -> Result<(), Error> {
let tool = self.doc["tool"].or_insert({
let mut tool = Table::new();
tool.set_implicit(true);
Item::Table(tool)
});
let tool_uv = tool["uv"].or_insert(Item::Table(Table::new()));

add_dependency(req, &mut tool_uv["dev-dependencies"])
pub fn add_dev_dependency(
&mut self,
req: &Requirement,
source: Option<&Source>,
) -> Result<(), Error> {
// Get or create `tool.uv`.
let tool_uv = self
.doc
.entry("tool")
.or_insert(implicit())
.as_table_mut()
.ok_or(Error::MalformedSources)?
.entry("uv")
.or_insert(Item::Table(Table::new()))
.as_table_mut()
.ok_or(Error::MalformedSources)?;

// Get or create the `tool.uv.dev-dependencies` array.
let dev_dependencies = tool_uv
.entry("dev-dependencies")
.or_insert(Item::Value(Value::Array(Array::new())))
.as_array_mut()
.ok_or(Error::MalformedDependencies)?;

add_dependency(req, dev_dependencies);

if let Some(source) = source {
// Get or create `tool.uv.sources`.
let sources = tool_uv
.entry("sources")
.or_insert(Item::Table(Table::new()))
.as_table_mut()
.ok_or(Error::MalformedSources)?;

add_source(req, source, sources);
}

Ok(())
}

/// Removes all occurrences of dependencies with the given name.
pub fn remove_dependency(&mut self, req: &PackageName) -> Result<Vec<Requirement>, Error> {
remove_dependency(req, &mut self.doc["project"]["dependencies"])
// Try to get `project.dependencies`.
let Some(dependencies) = self
.doc
.get_mut("project")
.and_then(Item::as_table_mut)
.and_then(|project| project.get_mut("dependencies"))
else {
return Ok(Vec::new());
};
let dependencies = dependencies
.as_array_mut()
.ok_or(Error::MalformedDependencies)?;

let requirements = remove_dependency(req, dependencies);

// Remove a matching source from `tool.uv.sources`, if it exists.
if let Some(sources) = self
.doc
.get_mut("tool")
.and_then(Item::as_table_mut)
.and_then(|tool| tool.get_mut("uv"))
.and_then(Item::as_table_mut)
.and_then(|tool_uv| tool_uv.get_mut("sources"))
{
let sources = sources.as_table_mut().ok_or(Error::MalformedSources)?;
sources.remove(req.as_ref());
}

Ok(requirements)
}

/// Removes all occurrences of development dependencies with the given name.
pub fn remove_dev_dependency(&mut self, req: &PackageName) -> Result<Vec<Requirement>, Error> {
let Some(tool_uv) = self.doc.get_mut("tool").and_then(|tool| tool.get_mut("uv")) else {
let Some(tool_uv) = self
.doc
.get_mut("tool")
.and_then(Item::as_table_mut)
.and_then(|tool| tool.get_mut("uv"))
.and_then(Item::as_table_mut)
else {
return Ok(Vec::new());
};

remove_dependency(req, &mut tool_uv["dev-dependencies"])
// Try to get `tool.uv.dev-dependencies`.
let Some(dev_dependencies) = tool_uv.get_mut("dev-dependencies") else {
return Ok(Vec::new());
};
let dev_dependencies = dev_dependencies
.as_array_mut()
.ok_or(Error::MalformedDependencies)?;

let requirements = remove_dependency(req, dev_dependencies);

// Remove a matching source from `tool.uv.sources`, if it exists.
if let Some(sources) = tool_uv.get_mut("sources") {
let sources = sources.as_table_mut().ok_or(Error::MalformedSources)?;
sources.remove(req.as_ref());
};

Ok(requirements)
}
}

/// Adds a dependency to the given `deps` array.
pub fn add_dependency(req: &Requirement, deps: &mut Item) -> Result<(), Error> {
let deps = deps
.or_insert(Item::Value(Value::Array(Array::new())))
.as_array_mut()
.ok_or(Error::MalformedDependencies)?;
/// Returns an implicit table.
fn implicit() -> Item {
let mut table = Table::new();
table.set_implicit(true);
Item::Table(table)
}

/// Adds a dependency to the given `deps` array.
pub fn add_dependency(req: &Requirement, deps: &mut Array) {
// Find matching dependencies.
let to_replace = find_dependencies(&req.name, deps);

if to_replace.is_empty() {
deps.push(req.to_string());
} else {
Expand All @@ -84,19 +204,11 @@ pub fn add_dependency(req: &Requirement, deps: &mut Item) -> Result<(), Error> {
deps.remove(i);
}
}

reformat_array_multiline(deps);
Ok(())
}

/// Removes all occurrences of dependencies with the given name from the given `deps` array.
fn remove_dependency(req: &PackageName, deps: &mut Item) -> Result<Vec<Requirement>, Error> {
if deps.is_none() {
return Ok(Vec::new());
}

let deps = deps.as_array_mut().ok_or(Error::MalformedDependencies)?;

fn remove_dependency(req: &PackageName, deps: &mut Array) -> Vec<Requirement> {
// Remove matching dependencies.
let removed = find_dependencies(req, deps)
.into_iter()
Expand All @@ -112,7 +224,7 @@ fn remove_dependency(req: &PackageName, deps: &mut Item) -> Result<Vec<Requireme
reformat_array_multiline(deps);
}

Ok(removed)
removed
}

// Returns a `Vec` containing the indices of all dependencies with the given name.
Expand All @@ -131,6 +243,24 @@ fn find_dependencies(name: &PackageName, deps: &Array) -> Vec<usize> {
to_replace
}

// 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!(),
}
}

impl fmt::Display for PyProjectTomlMut {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.doc.fmt(f)
Expand Down
4 changes: 4 additions & 0 deletions crates/uv/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1611,6 +1611,10 @@ pub(crate) struct AddArgs {
#[arg(long)]
pub(crate) dev: bool,

/// Add the requirements as workspace dependencies.
#[arg(long)]
pub(crate) workspace: bool,
Comment on lines +1614 to +1616
Copy link
Member

Choose a reason for hiding this comment

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

I'm on the fence whether we need an explicit argument for this, we could also do workspace discovery and know that it has to be a workspace dep, otoh it's consistent with us requiring workspace = true in pyproject.toml (it's something we took from cargo, it's helpful to make we don't accidentally flip between workspace and index and to keep the information about the package source local)


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

Expand Down
15 changes: 13 additions & 2 deletions crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
@@ -1,6 +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_mut::PyProjectTomlMut;
use uv_git::GitResolver;
use uv_requirements::{NamedRequirementsResolver, RequirementsSource, RequirementsSpecification};
Expand All @@ -24,6 +25,7 @@ use crate::settings::ResolverInstallerSettings;
#[allow(clippy::too_many_arguments)]
pub(crate) async fn add(
requirements: Vec<RequirementsSource>,
workspace: bool,
dev: bool,
python: Option<String>,
settings: ResolverInstallerSettings,
Expand Down Expand Up @@ -134,10 +136,19 @@ 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,
})
} else {
None
};

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

Expand Down
4 changes: 2 additions & 2 deletions crates/uv/src/commands/project/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pub(crate) async fn remove(
.filter(|deps| !deps.is_empty())
.is_some()
{
uv_warnings::warn_user!("`{req}` is not a development dependency; try calling `uv add` without the `--dev` flag");
uv_warnings::warn_user!("`{req}` is not a development dependency; try calling `uv remove` without the `--dev` flag");
}

anyhow::bail!("The dependency `{req}` could not be found in `dev-dependencies`");
Expand All @@ -65,7 +65,7 @@ pub(crate) async fn remove(
.is_some()
{
uv_warnings::warn_user!(
"`{req}` is a development dependency; try calling `uv add --dev`"
"`{req}` is a development dependency; try calling `uv remove --dev`"
);
}

Expand Down
9 changes: 2 additions & 7 deletions crates/uv/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -686,14 +686,9 @@ async fn run() -> Result<ExitStatus> {
// Initialize the cache.
let cache = cache.init()?.with_refresh(args.refresh);

let requirements = args
.requirements
.into_iter()
.map(RequirementsSource::Package)
.collect::<Vec<_>>();

commands::add(
requirements,
args.requirements,
args.workspace,
args.dev,
args.python,
args.settings,
Expand Down
11 changes: 10 additions & 1 deletion crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use uv_configuration::{
Upgrade,
};
use uv_normalize::PackageName;
use uv_requirements::RequirementsSource;
use uv_resolver::{AnnotationStyle, DependencyMode, ExcludeNewer, PreReleaseMode, ResolutionMode};
use uv_settings::{
Combine, FilesystemOptions, InstallerOptions, Options, PipOptions, ResolverInstallerOptions,
Expand Down Expand Up @@ -373,7 +374,8 @@ impl LockSettings {
#[allow(clippy::struct_excessive_bools, dead_code)]
#[derive(Debug, Clone)]
pub(crate) struct AddSettings {
pub(crate) requirements: Vec<String>,
pub(crate) requirements: Vec<RequirementsSource>,
pub(crate) workspace: bool,
pub(crate) dev: bool,
pub(crate) python: Option<String>,
pub(crate) refresh: Refresh,
Expand All @@ -387,14 +389,21 @@ impl AddSettings {
let AddArgs {
requirements,
dev,
workspace,
installer,
build,
refresh,
python,
} = args;

let requirements = requirements
.into_iter()
.map(RequirementsSource::Package)
.collect::<Vec<_>>();

Self {
requirements,
workspace,
dev,
python,
refresh: Refresh::from(refresh),
Expand Down
Loading
Loading