mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 21:35:00 +00:00
Allow uv add
to specify optional dependency groups (#4607)
## Summary Implements `uv add --optional <group>`, which adds a dependency to `project.optional-dependency.<group>`. Resolves https://github.com/astral-sh/uv/issues/4585.
This commit is contained in:
parent
9b38450998
commit
bbd59ff455
8 changed files with 443 additions and 113 deletions
|
@ -1763,9 +1763,13 @@ pub struct AddArgs {
|
|||
pub requirements: Vec<String>,
|
||||
|
||||
/// Add the requirements as development dependencies.
|
||||
#[arg(long)]
|
||||
#[arg(long, conflicts_with("optional"))]
|
||||
pub dev: bool,
|
||||
|
||||
/// Add the requirements to the specified optional dependency group.
|
||||
#[arg(long, conflicts_with("dev"))]
|
||||
pub optional: Option<ExtraName>,
|
||||
|
||||
/// Add the requirements as editables.
|
||||
#[arg(long, default_missing_value = "true", num_args(0..=1))]
|
||||
pub editable: Option<bool>,
|
||||
|
@ -1829,9 +1833,13 @@ pub struct RemoveArgs {
|
|||
pub requirements: Vec<PackageName>,
|
||||
|
||||
/// Remove the requirements from development dependencies.
|
||||
#[arg(long)]
|
||||
#[arg(long, conflicts_with("optional"))]
|
||||
pub dev: bool,
|
||||
|
||||
/// Remove the requirements from the specified optional dependency group.
|
||||
#[arg(long, conflicts_with("dev"))]
|
||||
pub optional: Option<ExtraName>,
|
||||
|
||||
/// Remove the dependency from a specific package in the workspace.
|
||||
#[arg(long, conflicts_with = "isolated")]
|
||||
pub package: Option<PackageName>,
|
||||
|
|
|
@ -289,6 +289,17 @@ impl Source {
|
|||
}
|
||||
}
|
||||
|
||||
/// The type of a dependency in a `pyproject.toml`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DependencyType {
|
||||
/// A dependency in `project.dependencies`.
|
||||
Production,
|
||||
/// A dependency in `tool.uv.dev-dependencies`.
|
||||
Dev,
|
||||
/// A dependency in `project.optional-dependencies.{0}`.
|
||||
Optional(ExtraName),
|
||||
}
|
||||
|
||||
/// <https://github.com/serde-rs/serde/issues/1316#issue-332908452>
|
||||
mod serde_from_and_to_string {
|
||||
use std::fmt::Display;
|
||||
|
|
|
@ -4,9 +4,9 @@ use std::{fmt, mem};
|
|||
use thiserror::Error;
|
||||
use toml_edit::{Array, DocumentMut, Item, RawString, Table, TomlError, Value};
|
||||
|
||||
use pep508_rs::{PackageName, Requirement, VersionOrUrl};
|
||||
use pep508_rs::{ExtraName, PackageName, Requirement, VersionOrUrl};
|
||||
|
||||
use crate::pyproject::{PyProjectToml, Source};
|
||||
use crate::pyproject::{DependencyType, PyProjectToml, Source};
|
||||
|
||||
/// Raw and mutable representation of a `pyproject.toml`.
|
||||
///
|
||||
|
@ -60,23 +60,7 @@ impl PyProjectTomlMut {
|
|||
add_dependency(req, dependencies, source.is_some())?;
|
||||
|
||||
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(&name, &source, sources)?;
|
||||
self.add_source(&name, &source)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -88,8 +72,8 @@ impl PyProjectTomlMut {
|
|||
req: Requirement,
|
||||
source: Option<Source>,
|
||||
) -> Result<(), Error> {
|
||||
// Get or create `tool.uv`.
|
||||
let tool_uv = self
|
||||
// Get or create `tool.uv.dev-dependencies`.
|
||||
let dev_dependencies = self
|
||||
.doc
|
||||
.entry("tool")
|
||||
.or_insert(implicit())
|
||||
|
@ -98,10 +82,7 @@ impl PyProjectTomlMut {
|
|||
.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
|
||||
.ok_or(Error::MalformedSources)?
|
||||
.entry("dev-dependencies")
|
||||
.or_insert(Item::Value(Value::Array(Array::new())))
|
||||
.as_array_mut()
|
||||
|
@ -111,82 +92,215 @@ impl PyProjectTomlMut {
|
|||
add_dependency(req, dev_dependencies, source.is_some())?;
|
||||
|
||||
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(&name, &source, sources)?;
|
||||
self.add_source(&name, &source)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds a dependency to `project.optional-dependencies`.
|
||||
pub fn add_optional_dependency(
|
||||
&mut self,
|
||||
req: Requirement,
|
||||
group: &ExtraName,
|
||||
source: Option<Source>,
|
||||
) -> Result<(), Error> {
|
||||
// Get or create `project.optional-dependencies`.
|
||||
let optional_dependencies = self
|
||||
.doc
|
||||
.entry("project")
|
||||
.or_insert(Item::Table(Table::new()))
|
||||
.as_table_mut()
|
||||
.ok_or(Error::MalformedDependencies)?
|
||||
.entry("optional-dependencies")
|
||||
.or_insert(Item::Table(Table::new()))
|
||||
.as_table_mut()
|
||||
.ok_or(Error::MalformedDependencies)?;
|
||||
|
||||
let group = optional_dependencies
|
||||
.entry(group.as_ref())
|
||||
.or_insert(Item::Value(Value::Array(Array::new())))
|
||||
.as_array_mut()
|
||||
.ok_or(Error::MalformedDependencies)?;
|
||||
|
||||
let name = req.name.clone();
|
||||
add_dependency(req, group, source.is_some())?;
|
||||
|
||||
if let Some(source) = source {
|
||||
self.add_source(&name, &source)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds a source to `tool.uv.sources`.
|
||||
fn add_source(&mut self, name: &PackageName, source: &Source) -> Result<(), Error> {
|
||||
// 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(name, source, sources)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Removes all occurrences of dependencies with the given name.
|
||||
pub fn remove_dependency(&mut self, req: &PackageName) -> Result<Vec<Requirement>, Error> {
|
||||
// Try to get `project.dependencies`.
|
||||
let Some(dependencies) = self
|
||||
.doc
|
||||
.get_mut("project")
|
||||
.and_then(Item::as_table_mut)
|
||||
.map(|project| project.as_table_mut().ok_or(Error::MalformedSources))
|
||||
.transpose()?
|
||||
.and_then(|project| project.get_mut("dependencies"))
|
||||
.map(|dependencies| dependencies.as_array_mut().ok_or(Error::MalformedSources))
|
||||
.transpose()?
|
||||
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());
|
||||
}
|
||||
self.remove_source(req)?;
|
||||
|
||||
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
|
||||
// Try to get `tool.uv.dev-dependencies`.
|
||||
let Some(dev_dependencies) = self
|
||||
.doc
|
||||
.get_mut("tool")
|
||||
.and_then(Item::as_table_mut)
|
||||
.map(|tool| tool.as_table_mut().ok_or(Error::MalformedSources))
|
||||
.transpose()?
|
||||
.and_then(|tool| tool.get_mut("uv"))
|
||||
.and_then(Item::as_table_mut)
|
||||
.map(|tool_uv| tool_uv.as_table_mut().ok_or(Error::MalformedSources))
|
||||
.transpose()?
|
||||
.and_then(|tool_uv| tool_uv.get_mut("dev-dependencies"))
|
||||
.map(|dependencies| dependencies.as_array_mut().ok_or(Error::MalformedSources))
|
||||
.transpose()?
|
||||
else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
|
||||
// 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());
|
||||
};
|
||||
self.remove_source(req)?;
|
||||
|
||||
Ok(requirements)
|
||||
}
|
||||
|
||||
/// Removes all occurrences of optional dependencies in the group with the given name.
|
||||
pub fn remove_optional_dependency(
|
||||
&mut self,
|
||||
req: &PackageName,
|
||||
group: &ExtraName,
|
||||
) -> Result<Vec<Requirement>, Error> {
|
||||
// Try to get `project.optional-dependencies.<group>`.
|
||||
let Some(optional_dependencies) = self
|
||||
.doc
|
||||
.get_mut("project")
|
||||
.map(|project| project.as_table_mut().ok_or(Error::MalformedSources))
|
||||
.transpose()?
|
||||
.and_then(|project| project.get_mut("optional-dependencies"))
|
||||
.map(|extras| extras.as_table_mut().ok_or(Error::MalformedSources))
|
||||
.transpose()?
|
||||
.and_then(|extras| extras.get_mut(group.as_ref()))
|
||||
.map(|dependencies| dependencies.as_array_mut().ok_or(Error::MalformedSources))
|
||||
.transpose()?
|
||||
else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
|
||||
let requirements = remove_dependency(req, optional_dependencies);
|
||||
self.remove_source(req)?;
|
||||
|
||||
Ok(requirements)
|
||||
}
|
||||
|
||||
// Remove a matching source from `tool.uv.sources`, if it exists.
|
||||
fn remove_source(&mut self, name: &PackageName) -> Result<(), Error> {
|
||||
if let Some(sources) = self
|
||||
.doc
|
||||
.get_mut("tool")
|
||||
.map(|tool| tool.as_table_mut().ok_or(Error::MalformedSources))
|
||||
.transpose()?
|
||||
.and_then(|tool| tool.get_mut("uv"))
|
||||
.map(|tool_uv| tool_uv.as_table_mut().ok_or(Error::MalformedSources))
|
||||
.transpose()?
|
||||
.and_then(|tool_uv| tool_uv.get_mut("sources"))
|
||||
.map(|sources| sources.as_table_mut().ok_or(Error::MalformedSources))
|
||||
.transpose()?
|
||||
{
|
||||
sources.remove(name.as_ref());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns all the places in this `pyproject.toml` that contain a dependency with the given
|
||||
/// name.
|
||||
///
|
||||
/// This method searches `project.dependencies`, `tool.uv.dev-dependencies`, and
|
||||
/// `tool.uv.optional-dependencies`.
|
||||
pub fn find_dependency(&self, name: &PackageName) -> Vec<DependencyType> {
|
||||
let mut types = Vec::new();
|
||||
|
||||
if let Some(project) = self.doc.get("project").and_then(Item::as_table) {
|
||||
// Check `project.dependencies`.
|
||||
if let Some(dependencies) = project.get("dependencies").and_then(Item::as_array) {
|
||||
if !find_dependencies(name, dependencies).is_empty() {
|
||||
types.push(DependencyType::Production);
|
||||
}
|
||||
}
|
||||
|
||||
// Check `project.optional-dependencies`.
|
||||
if let Some(extras) = project
|
||||
.get("optional-dependencies")
|
||||
.and_then(Item::as_table)
|
||||
{
|
||||
for (extra, dependencies) in extras {
|
||||
let Some(dependencies) = dependencies.as_array() else {
|
||||
continue;
|
||||
};
|
||||
let Ok(extra) = ExtraName::new(extra.to_string()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !find_dependencies(name, dependencies).is_empty() {
|
||||
types.push(DependencyType::Optional(extra));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check `tool.uv.dev-dependencies`.
|
||||
if let Some(dev_dependencies) = self
|
||||
.doc
|
||||
.get("tool")
|
||||
.and_then(Item::as_table)
|
||||
.and_then(|tool| tool.get("uv"))
|
||||
.and_then(Item::as_table)
|
||||
.and_then(|tool| tool.get("dev-dependencies"))
|
||||
.and_then(Item::as_array)
|
||||
{
|
||||
if !find_dependencies(name, dev_dependencies).is_empty() {
|
||||
types.push(DependencyType::Dev);
|
||||
}
|
||||
}
|
||||
|
||||
types
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an implicit table.
|
||||
|
|
|
@ -5,7 +5,7 @@ use uv_cache::Cache;
|
|||
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
|
||||
use uv_configuration::{Concurrency, ExtrasSpecification, PreviewMode, SetupPyStrategy};
|
||||
use uv_dispatch::BuildDispatch;
|
||||
use uv_distribution::pyproject::{Source, SourceError};
|
||||
use uv_distribution::pyproject::{DependencyType, Source, SourceError};
|
||||
use uv_distribution::pyproject_mut::PyProjectTomlMut;
|
||||
use uv_distribution::{DistributionDatabase, ProjectWorkspace, Workspace};
|
||||
use uv_git::GitResolver;
|
||||
|
@ -27,8 +27,8 @@ use crate::settings::ResolverInstallerSettings;
|
|||
#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
|
||||
pub(crate) async fn add(
|
||||
requirements: Vec<RequirementsSource>,
|
||||
dev: bool,
|
||||
editable: Option<bool>,
|
||||
dependency_type: DependencyType,
|
||||
raw_sources: bool,
|
||||
rev: Option<String>,
|
||||
tag: Option<String>,
|
||||
|
@ -186,10 +186,16 @@ pub(crate) async fn add(
|
|||
(req, source)
|
||||
};
|
||||
|
||||
if dev {
|
||||
pyproject.add_dev_dependency(req, source)?;
|
||||
} else {
|
||||
pyproject.add_dependency(req, source)?;
|
||||
match dependency_type {
|
||||
DependencyType::Production => {
|
||||
pyproject.add_dependency(req, source)?;
|
||||
}
|
||||
DependencyType::Dev => {
|
||||
pyproject.add_dev_dependency(req, source)?;
|
||||
}
|
||||
DependencyType::Optional(ref group) => {
|
||||
pyproject.add_optional_dependency(req, group, source)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ use pep508_rs::PackageName;
|
|||
use uv_cache::Cache;
|
||||
use uv_client::Connectivity;
|
||||
use uv_configuration::{Concurrency, ExtrasSpecification, PreviewMode};
|
||||
use uv_distribution::pyproject::DependencyType;
|
||||
use uv_distribution::pyproject_mut::PyProjectTomlMut;
|
||||
use uv_distribution::{ProjectWorkspace, Workspace};
|
||||
use uv_toolchain::{ToolchainPreference, ToolchainRequest};
|
||||
|
@ -18,7 +19,7 @@ use crate::settings::{InstallerSettings, ResolverSettings};
|
|||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn remove(
|
||||
requirements: Vec<PackageName>,
|
||||
dev: bool,
|
||||
dependency_type: DependencyType,
|
||||
package: Option<PackageName>,
|
||||
python: Option<String>,
|
||||
toolchain_preference: ToolchainPreference,
|
||||
|
@ -45,41 +46,33 @@ pub(crate) async fn remove(
|
|||
|
||||
let mut pyproject = PyProjectTomlMut::from_toml(project.current_project().pyproject_toml())?;
|
||||
for req in requirements {
|
||||
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()
|
||||
{
|
||||
warn_user!("`{req}` is not a development dependency; try calling `uv remove` without the `--dev` flag");
|
||||
match dependency_type {
|
||||
DependencyType::Production => {
|
||||
let deps = pyproject.remove_dependency(&req)?;
|
||||
if deps.is_empty() {
|
||||
warn_if_present(&req, &pyproject);
|
||||
anyhow::bail!("The dependency `{req}` could not be found in `dependencies`");
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
warn_user!("`{req}` is a development dependency; try calling `uv remove --dev`");
|
||||
DependencyType::Dev => {
|
||||
let deps = pyproject.remove_dev_dependency(&req)?;
|
||||
if deps.is_empty() {
|
||||
warn_if_present(&req, &pyproject);
|
||||
anyhow::bail!(
|
||||
"The dependency `{req}` could not be found in `dev-dependencies`"
|
||||
);
|
||||
}
|
||||
}
|
||||
DependencyType::Optional(ref group) => {
|
||||
let deps = pyproject.remove_optional_dependency(&req, group)?;
|
||||
if deps.is_empty() {
|
||||
warn_if_present(&req, &pyproject);
|
||||
anyhow::bail!(
|
||||
"The dependency `{req}` could not be found in `optional-dependencies`"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::bail!("The dependency `{req}` could not be found in `dependencies`");
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Save the modified `pyproject.toml`.
|
||||
|
@ -143,3 +136,25 @@ pub(crate) async fn remove(
|
|||
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
|
||||
/// Emit a warning if a dependency with the given name is present as any dependency type.
|
||||
///
|
||||
/// This is useful when a dependency of the user-specified type was not found, but it may be present
|
||||
/// elsewhere.
|
||||
fn warn_if_present(name: &PackageName, pyproject: &PyProjectTomlMut) {
|
||||
for dep_ty in pyproject.find_dependency(name) {
|
||||
match dep_ty {
|
||||
DependencyType::Production => {
|
||||
warn_user!("`{name}` is a production dependency");
|
||||
}
|
||||
DependencyType::Dev => {
|
||||
warn_user!("`{name}` is a development dependency; try calling `uv remove --dev`");
|
||||
}
|
||||
DependencyType::Optional(group) => {
|
||||
warn_user!(
|
||||
"`{name}` is an optional dependency; try calling `uv remove --optional {group}`"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -721,8 +721,8 @@ async fn run() -> Result<ExitStatus> {
|
|||
|
||||
commands::add(
|
||||
args.requirements,
|
||||
args.dev,
|
||||
args.editable,
|
||||
args.dependency_type,
|
||||
args.raw_sources,
|
||||
args.rev,
|
||||
args.tag,
|
||||
|
@ -751,7 +751,7 @@ async fn run() -> Result<ExitStatus> {
|
|||
|
||||
commands::remove(
|
||||
args.requirements,
|
||||
args.dev,
|
||||
args.dependency_type,
|
||||
args.package,
|
||||
args.python,
|
||||
globals.toolchain_preference,
|
||||
|
|
|
@ -22,6 +22,7 @@ use uv_configuration::{
|
|||
KeyringProviderType, NoBinary, NoBuild, PreviewMode, Reinstall, SetupPyStrategy, TargetTriple,
|
||||
Upgrade,
|
||||
};
|
||||
use uv_distribution::pyproject::DependencyType;
|
||||
use uv_normalize::PackageName;
|
||||
use uv_requirements::RequirementsSource;
|
||||
use uv_resolver::{AnnotationStyle, DependencyMode, ExcludeNewer, PreReleaseMode, ResolutionMode};
|
||||
|
@ -434,7 +435,7 @@ impl LockSettings {
|
|||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AddSettings {
|
||||
pub(crate) requirements: Vec<RequirementsSource>,
|
||||
pub(crate) dev: bool,
|
||||
pub(crate) dependency_type: DependencyType,
|
||||
pub(crate) editable: Option<bool>,
|
||||
pub(crate) extras: Vec<ExtraName>,
|
||||
pub(crate) raw_sources: bool,
|
||||
|
@ -454,6 +455,7 @@ impl AddSettings {
|
|||
let AddArgs {
|
||||
requirements,
|
||||
dev,
|
||||
optional,
|
||||
editable,
|
||||
extra,
|
||||
raw_sources,
|
||||
|
@ -472,9 +474,17 @@ impl AddSettings {
|
|||
.map(RequirementsSource::Package)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let dependency_type = if let Some(group) = optional {
|
||||
DependencyType::Optional(group)
|
||||
} else if dev {
|
||||
DependencyType::Dev
|
||||
} else {
|
||||
DependencyType::Production
|
||||
};
|
||||
|
||||
Self {
|
||||
requirements,
|
||||
dev,
|
||||
dependency_type,
|
||||
editable,
|
||||
raw_sources,
|
||||
rev,
|
||||
|
@ -497,7 +507,7 @@ impl AddSettings {
|
|||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct RemoveSettings {
|
||||
pub(crate) requirements: Vec<PackageName>,
|
||||
pub(crate) dev: bool,
|
||||
pub(crate) dependency_type: DependencyType,
|
||||
pub(crate) package: Option<PackageName>,
|
||||
pub(crate) python: Option<String>,
|
||||
}
|
||||
|
@ -508,14 +518,23 @@ impl RemoveSettings {
|
|||
pub(crate) fn resolve(args: RemoveArgs, _filesystem: Option<FilesystemOptions>) -> Self {
|
||||
let RemoveArgs {
|
||||
dev,
|
||||
optional,
|
||||
requirements,
|
||||
package,
|
||||
python,
|
||||
} = args;
|
||||
|
||||
let dependency_type = if let Some(group) = optional {
|
||||
DependencyType::Optional(group)
|
||||
} else if dev {
|
||||
DependencyType::Dev
|
||||
} else {
|
||||
DependencyType::Production
|
||||
};
|
||||
|
||||
Self {
|
||||
requirements,
|
||||
dev,
|
||||
dependency_type,
|
||||
package,
|
||||
python,
|
||||
}
|
||||
|
|
|
@ -718,6 +718,163 @@ fn add_remove_dev() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Add and remove an optional dependency.
|
||||
#[test]
|
||||
fn add_remove_optional() -> 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 = []
|
||||
"#})?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.add(&["anyio==3.7.0"]).arg("--optional=io"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv add` is experimental and may change without warning.
|
||||
Resolved 1 package in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
"###);
|
||||
|
||||
let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
pyproject_toml, @r###"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
|
||||
[project.optional-dependencies]
|
||||
io = [
|
||||
"anyio==3.7.0",
|
||||
]
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
// `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"
|
||||
|
||||
[[distribution]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = "editable+."
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
// Install from the lockfile.
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv sync` is experimental and may change without warning.
|
||||
Audited 1 package in [TIME]
|
||||
"###);
|
||||
|
||||
// This should fail without --optional.
|
||||
uv_snapshot!(context.filters(), context.remove(&["anyio"]), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv remove` is experimental and may change without warning.
|
||||
warning: `anyio` is an optional dependency; try calling `uv remove --optional io`
|
||||
error: The dependency `anyio` could not be found in `dependencies`
|
||||
"###);
|
||||
|
||||
// Remove the dependency.
|
||||
uv_snapshot!(context.filters(), context.remove(&["anyio"]).arg("--optional=io"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv remove` is experimental and may change without warning.
|
||||
Resolved 1 package in [TIME]
|
||||
Prepared 1 package in [TIME]
|
||||
Uninstalled 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
- project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
+ project==0.1.0 (from file://[TEMP_DIR]/)
|
||||
"###);
|
||||
|
||||
let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(
|
||||
pyproject_toml, @r###"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
|
||||
[project.optional-dependencies]
|
||||
io = []
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
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"
|
||||
|
||||
[[distribution]]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
source = "editable+."
|
||||
"###
|
||||
);
|
||||
});
|
||||
|
||||
// Install from the lockfile.
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv sync` is experimental and may change without warning.
|
||||
Audited 1 package in [TIME]
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add and remove a workspace dependency.
|
||||
#[test]
|
||||
fn add_remove_workspace() -> Result<()> {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue