Skip to content

Commit

Permalink
add support for adding/removing development dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
ibraheemdev committed Jun 14, 2024
1 parent 221f364 commit 7cca96f
Show file tree
Hide file tree
Showing 8 changed files with 399 additions and 73 deletions.
139 changes: 81 additions & 58 deletions crates/uv-distribution/src/pyproject_mut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::fmt;
use std::str::FromStr;

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

use pep508_rs::{PackageName, Requirement};
use pypi_types::VerbatimParsedUrl;
Expand Down Expand Up @@ -33,79 +33,102 @@ impl PyProjectTomlMut {
})
}

/// Adds a dependency.
/// Adds a dependency to `project.dependencies`.
pub fn add_dependency(&mut self, req: &Requirement) -> Result<(), Error> {
let deps = &mut self.doc["project"]["dependencies"];
if deps.is_none() {
*deps = Item::Value(Value::Array(Array::new()));
}
let deps = deps.as_array_mut().ok_or(Error::MalformedDependencies)?;

// Try to find matching dependencies.
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 == req.name)
.is_some()
{
to_replace.push(i);
}
}
add_dependency(req, &mut self.doc["project"]["dependencies"])
}

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

reformat_array_multiline(deps);
Ok(())
add_dependency(req, &mut tool_uv["dev-dependencies"])
}

/// Removes all occurrences of dependencies with the given name.
pub fn remove_dependency(&mut self, req: &PackageName) -> Result<Vec<Requirement>, Error> {
let deps = &mut self.doc["project"]["dependencies"];
if deps.is_none() {
remove_dependency(req, &mut self.doc["project"]["dependencies"])
}

/// 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 {
return Ok(Vec::new());
};

remove_dependency(req, &mut tool_uv["dev-dependencies"])
}
}

/// 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)?;

// 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);
}
}

reformat_array_multiline(deps);
Ok(())
}

let deps = deps.as_array_mut().ok_or(Error::MalformedDependencies)?;
/// 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());
}

// Try to find matching dependencies.
let mut to_remove = Vec::new();
for (i, dep) in deps.iter().enumerate() {
if dep
let deps = deps.as_array_mut().ok_or(Error::MalformedDependencies)?;

// Remove matching dependencies.
let removed = find_dependencies(req, deps)
.into_iter()
.rev() // Reverse to preserve indices as we remove them.
.filter_map(|i| {
deps.remove(i)
.as_str()
.and_then(try_parse_requirement)
.filter(|dep| dep.name == *req)
.is_some()
{
to_remove.push(i);
}
}
.and_then(|req| Requirement::from_str(req).ok())
})
.collect::<Vec<_>>();

let removed = to_remove
.into_iter()
.rev() // Reverse to preserve indices as we remove them.
.filter_map(|i| {
deps.remove(i)
.as_str()
.and_then(|req| Requirement::from_str(req).ok())
})
.collect::<Vec<_>>();
if !removed.is_empty() {
reformat_array_multiline(deps);
}

if !removed.is_empty() {
reformat_array_multiline(deps);
}
Ok(removed)
}

Ok(removed)
// Returns a `Vec` containing the indices of all dependencies with the given name.
fn find_dependencies(name: &PackageName, deps: &Array) -> Vec<usize> {
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);
}
}
to_replace
}

impl fmt::Display for PyProjectTomlMut {
Expand Down
8 changes: 8 additions & 0 deletions crates/uv/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1602,6 +1602,10 @@ pub(crate) struct AddArgs {
#[arg(required = true)]
pub(crate) requirements: Vec<String>,

/// Add the requirements as development dependencies.
#[arg(long)]
pub(crate) dev: bool,

#[command(flatten)]
pub(crate) resolver: ResolverArgs,

Expand Down Expand Up @@ -1634,6 +1638,10 @@ pub(crate) struct RemoveArgs {
#[arg(required = true)]
pub(crate) requirements: Vec<PackageName>,

/// Remove the requirements from development dependencies.
#[arg(long)]
pub(crate) dev: bool,

/// The Python interpreter into which packages should be installed.
///
/// By default, `uv` installs into the virtual environment in the current working directory or
Expand Down
9 changes: 7 additions & 2 deletions crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use crate::settings::{InstallerSettings, ResolverSettings};
#[allow(clippy::too_many_arguments)]
pub(crate) async fn add(
requirements: Vec<RequirementsSource>,
dev: bool,
python: Option<String>,
settings: ResolverSettings,
preview: PreviewMode,
Expand Down Expand Up @@ -125,8 +126,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 {
pyproject.add_dependency(&pep508_rs::Requirement::from(req))?;
for req in requirements.into_iter().map(pep508_rs::Requirement::from) {
if dev {
pyproject.add_dev_dependency(&req)?;
} else {
pyproject.add_dependency(&req)?;
}
}

// Save the modified `pyproject.toml`.
Expand Down
42 changes: 37 additions & 5 deletions crates/uv/src/commands/project/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use crate::settings::{InstallerSettings, ResolverSettings};
#[allow(clippy::too_many_arguments)]
pub(crate) async fn remove(
requirements: Vec<PackageName>,
dev: bool,
python: Option<String>,
preview: PreviewMode,
connectivity: Connectivity,
Expand All @@ -33,12 +34,43 @@ pub(crate) async fn remove(

let mut pyproject = PyProjectTomlMut::from_toml(project.current_project().pyproject_toml())?;
for req in requirements {
if pyproject.remove_dependency(&req)?.is_empty() {
anyhow::bail!(
"The dependency `{}` could not be found in `dependencies`",
req
);
if dev {
let deps = pyproject.remove_dev_dependency(&req)?;
if deps.is_empty() {
// Check if there is a matching regular dependency.
if pyproject
.remove_dependency(&req)
.ok()
.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");
}

anyhow::bail!("The dependency `{req}` could not be found in `dev-dependencies`");
}

continue;
}

let deps = pyproject.remove_dependency(&req)?;
if deps.is_empty() {
// Check if there is a matching development dependency.
if pyproject
.remove_dev_dependency(&req)
.ok()
.filter(|deps| !deps.is_empty())
.is_some()
{
uv_warnings::warn_user!(
"`{req}` is a development dependency, did you mean to call `uv add --dev`?"
);
}

anyhow::bail!("The dependency `{req}` could not be found in `dependencies`");
}

continue;
}

// Save the modified `pyproject.toml`.
Expand Down
2 changes: 2 additions & 0 deletions crates/uv/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,7 @@ async fn run() -> Result<ExitStatus> {

commands::add(
requirements,
args.dev,
args.python,
args.settings,
globals.preview,
Expand All @@ -714,6 +715,7 @@ async fn run() -> Result<ExitStatus> {

commands::remove(
args.requirements,
args.dev,
args.python,
globals.preview,
globals.connectivity,
Expand Down
6 changes: 6 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ impl LockSettings {
#[derive(Debug, Clone)]
pub(crate) struct AddSettings {
pub(crate) requirements: Vec<String>,
pub(crate) dev: bool,
pub(crate) python: Option<String>,
pub(crate) refresh: Refresh,
pub(crate) settings: ResolverSettings,
Expand All @@ -357,6 +358,7 @@ impl AddSettings {
pub(crate) fn resolve(args: AddArgs, filesystem: Option<FilesystemOptions>) -> Self {
let AddArgs {
requirements,
dev,
resolver,
build,
refresh,
Expand All @@ -365,6 +367,7 @@ impl AddSettings {

Self {
requirements,
dev,
python,
refresh: Refresh::from(refresh),
settings: ResolverSettings::combine(resolver_options(resolver, build), filesystem),
Expand All @@ -377,6 +380,7 @@ impl AddSettings {
#[derive(Debug, Clone)]
pub(crate) struct RemoveSettings {
pub(crate) requirements: Vec<PackageName>,
pub(crate) dev: bool,
pub(crate) python: Option<String>,
}

Expand All @@ -385,11 +389,13 @@ impl RemoveSettings {
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn resolve(args: RemoveArgs, _filesystem: Option<FilesystemOptions>) -> Self {
let RemoveArgs {
dev,
requirements,
python,
} = args;

Self {
dev,
requirements,
python,
}
Expand Down
12 changes: 10 additions & 2 deletions crates/uv/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ impl TestContext {
}

/// Create a `uv add` command for the given requirements.
pub fn add(&self, reqs: &[&str]) -> std::process::Command {
pub fn add(&self, reqs: &[&str], dev: bool) -> std::process::Command {
let mut command = std::process::Command::new(get_bin());
command
.arg("add")
Expand All @@ -332,6 +332,10 @@ impl TestContext {
.env("UV_NO_WRAP", "1")
.current_dir(&self.temp_dir);

if dev {
command.arg("--dev");
}

if cfg!(all(windows, debug_assertions)) {
// TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the
// default windows stack of 1MB
Expand All @@ -342,7 +346,7 @@ impl TestContext {
}

/// Create a `uv remove` command for the given requirements.
pub fn remove(&self, reqs: &[&str]) -> std::process::Command {
pub fn remove(&self, reqs: &[&str], dev: bool) -> std::process::Command {
let mut command = std::process::Command::new(get_bin());
command
.arg("remove")
Expand All @@ -353,6 +357,10 @@ impl TestContext {
.env("UV_NO_WRAP", "1")
.current_dir(&self.temp_dir);

if dev {
command.arg("--dev");
}

if cfg!(all(windows, debug_assertions)) {
// TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the
// default windows stack of 1MB
Expand Down
Loading

0 comments on commit 7cca96f

Please sign in to comment.