Skip to content

Commit 2aa4855

Browse files
Add to dependency-groups.dev in uv add --dev (#8570)
## Summary `uv add --dev` now updates the `dependency-groups.dev` section, rather than `tool.uv.dev-dependencies` -- unless the dependency is already present in `tool.uv.dev-dependencies`. `uv remove --dev` now removes from both `dependency-groups.dev` and `tool.uv.dev-dependencies`. `--dev` and `--group dev` are now treated equivalently in `uv add` and `uv remove`.
1 parent a6b83fa commit 2aa4855

File tree

4 files changed

+519
-31
lines changed

4 files changed

+519
-31
lines changed

crates/uv-workspace/src/pyproject_mut.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,26 @@ impl PyProjectTomlMut {
782782
Ok(())
783783
}
784784

785+
/// Returns `true` if the `tool.uv.dev-dependencies` table is present.
786+
pub fn has_dev_dependencies(&self) -> bool {
787+
self.doc
788+
.get("tool")
789+
.and_then(Item::as_table)
790+
.and_then(|tool| tool.get("uv"))
791+
.and_then(Item::as_table)
792+
.and_then(|uv| uv.get("dev-dependencies"))
793+
.is_some()
794+
}
795+
796+
/// Returns `true` if the `dependency-groups` table is present and contains the given group.
797+
pub fn has_dependency_group(&self, group: &GroupName) -> bool {
798+
self.doc
799+
.get("dependency-groups")
800+
.and_then(Item::as_table)
801+
.and_then(|groups| groups.get(group.as_ref()))
802+
.is_some()
803+
}
804+
785805
/// Returns all the places in this `pyproject.toml` that contain a dependency with the given
786806
/// name.
787807
///

crates/uv/src/commands/project/add.rs

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use uv_distribution::DistributionDatabase;
2121
use uv_distribution_types::{Index, IndexName, UnresolvedRequirement, VersionId};
2222
use uv_fs::Simplified;
2323
use uv_git::{GitReference, GIT_STORE};
24-
use uv_normalize::PackageName;
24+
use uv_normalize::{PackageName, DEV_DEPENDENCIES};
2525
use uv_pep508::{ExtraName, Requirement, UnnamedRequirement, VersionOrUrl};
2626
use uv_pypi_types::{redact_credentials, ParsedUrl, RequirementSource, VerbatimParsedUrl};
2727
use uv_python::{
@@ -203,8 +203,12 @@ pub(crate) async fn add(
203203
DependencyType::Optional(_) => {
204204
bail!("Project is missing a `[project]` table; add a `[project]` table to use optional dependencies, or run `{}` instead", "uv add --dev".green())
205205
}
206+
DependencyType::Group(_) => {
207+
// TODO(charlie): Allow adding to `dependency-groups` in non-`[project]`
208+
// targets, per PEP 735.
209+
bail!("Project is missing a `[project]` table; add a `[project]` table to use `dependency-groups` dependencies, or run `{}` instead", "uv add --dev".green())
210+
}
206211
DependencyType::Dev => (),
207-
DependencyType::Group(_) => (),
208212
}
209213
}
210214

@@ -463,8 +467,49 @@ pub(crate) async fn add(
463467
_ => source,
464468
};
465469

470+
// Determine the dependency type.
471+
let dependency_type = match &dependency_type {
472+
DependencyType::Dev => {
473+
let existing = toml.find_dependency(&requirement.name, None);
474+
if existing.iter().any(|dependency_type| matches!(dependency_type, DependencyType::Group(group) if group == &*DEV_DEPENDENCIES)) {
475+
// If the dependency already exists in `dependency-groups.dev`, use that.
476+
DependencyType::Group(DEV_DEPENDENCIES.clone())
477+
} else if existing.iter().any(|dependency_type| matches!(dependency_type, DependencyType::Dev)) {
478+
// If the dependency already exists in `dev-dependencies`, use that.
479+
DependencyType::Dev
480+
} else if target.as_project().is_some_and(uv_workspace::VirtualProject::is_non_project) {
481+
// TODO(charlie): Allow adding to `dependency-groups` in non-`[project]` targets.
482+
DependencyType::Dev
483+
} else {
484+
// Otherwise, use `dependency-groups.dev`, unless it would introduce a separate table.
485+
match (toml.has_dev_dependencies(), toml.has_dependency_group(&DEV_DEPENDENCIES)) {
486+
(true, false) => DependencyType::Dev,
487+
(false, true) => DependencyType::Group(DEV_DEPENDENCIES.clone()),
488+
(true, true) => DependencyType::Group(DEV_DEPENDENCIES.clone()),
489+
(false, false) => DependencyType::Group(DEV_DEPENDENCIES.clone()),
490+
}
491+
}
492+
}
493+
DependencyType::Group(group) if group == &*DEV_DEPENDENCIES => {
494+
let existing = toml.find_dependency(&requirement.name, None);
495+
if existing.iter().any(|dependency_type| matches!(dependency_type, DependencyType::Group(group) if group == &*DEV_DEPENDENCIES)) {
496+
// If the dependency already exists in `dependency-groups.dev`, use that.
497+
DependencyType::Group(DEV_DEPENDENCIES.clone())
498+
} else if existing.iter().any(|dependency_type| matches!(dependency_type, DependencyType::Dev)) {
499+
// If the dependency already exists in `dev-dependencies`, use that.
500+
DependencyType::Dev
501+
} else {
502+
// Otherwise, use `dependency-groups.dev`.
503+
DependencyType::Group(DEV_DEPENDENCIES.clone())
504+
}
505+
}
506+
DependencyType::Production => DependencyType::Production,
507+
DependencyType::Optional(extra) => DependencyType::Optional(extra.clone()),
508+
DependencyType::Group(group) => DependencyType::Group(group.clone()),
509+
};
510+
466511
// Update the `pyproject.toml`.
467-
let edit = match dependency_type {
512+
let edit = match &dependency_type {
468513
DependencyType::Production => toml.add_dependency(&requirement, source.as_ref())?,
469514
DependencyType::Dev => toml.add_dev_dependency(&requirement, source.as_ref())?,
470515
DependencyType::Optional(ref extra) => {
@@ -478,7 +523,7 @@ pub(crate) async fn add(
478523
// If the edit was inserted before the end of the list, update the existing edits.
479524
if let ArrayEdit::Add(index) = &edit {
480525
for edit in &mut edits {
481-
if *edit.dependency_type == dependency_type {
526+
if edit.dependency_type == dependency_type {
482527
match &mut edit.edit {
483528
ArrayEdit::Add(existing) => {
484529
if *existing >= *index {
@@ -496,7 +541,7 @@ pub(crate) async fn add(
496541
}
497542

498543
edits.push(DependencyEdit {
499-
dependency_type: &dependency_type,
544+
dependency_type,
500545
requirement,
501546
source,
502547
edit,
@@ -646,7 +691,7 @@ pub(crate) async fn add(
646691
async fn lock_and_sync(
647692
mut project: VirtualProject,
648693
toml: &mut PyProjectTomlMut,
649-
edits: &[DependencyEdit<'_>],
694+
edits: &[DependencyEdit],
650695
venv: &PythonEnvironment,
651696
state: SharedState,
652697
locked: bool,
@@ -973,6 +1018,14 @@ enum Target {
9731018
}
9741019

9751020
impl Target {
1021+
/// Returns the [`VirtualProject`] for the target, if it is a project.
1022+
fn as_project(&self) -> Option<&VirtualProject> {
1023+
match self {
1024+
Self::Project(project, _) => Some(project),
1025+
Self::Script(_, _) => None,
1026+
}
1027+
}
1028+
9761029
/// Returns the [`Interpreter`] for the target.
9771030
fn interpreter(&self) -> &Interpreter {
9781031
match self {
@@ -983,8 +1036,8 @@ impl Target {
9831036
}
9841037

9851038
#[derive(Debug, Clone)]
986-
struct DependencyEdit<'a> {
987-
dependency_type: &'a DependencyType,
1039+
struct DependencyEdit {
1040+
dependency_type: DependencyType,
9881041
requirement: Requirement,
9891042
source: Option<Source>,
9901043
edit: ArrayEdit,

crates/uv/src/commands/project/remove.rs

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use uv_configuration::{
99
Concurrency, DevGroupsManifest, EditableMode, ExtrasSpecification, InstallOptions, LowerBound,
1010
};
1111
use uv_fs::Simplified;
12+
use uv_normalize::DEV_DEPENDENCIES;
1213
use uv_pep508::PackageName;
1314
use uv_python::{PythonDownloads, PythonPreference, PythonRequest};
1415
use uv_scripts::Pep723Script;
@@ -106,11 +107,13 @@ pub(crate) async fn remove(
106107
}
107108
}
108109
DependencyType::Dev => {
109-
let deps = toml.remove_dev_dependency(&package)?;
110-
if deps.is_empty() {
110+
let dev_deps = toml.remove_dev_dependency(&package)?;
111+
let group_deps =
112+
toml.remove_dependency_group_requirement(&package, &DEV_DEPENDENCIES)?;
113+
if dev_deps.is_empty() && group_deps.is_empty() {
111114
warn_if_present(&package, &toml);
112115
anyhow::bail!(
113-
"The dependency `{package}` could not be found in `dev-dependencies`"
116+
"The dependency `{package}` could not be found in `dev-dependencies` or `dependency-groups.dev`"
114117
);
115118
}
116119
}
@@ -124,12 +127,24 @@ pub(crate) async fn remove(
124127
}
125128
}
126129
DependencyType::Group(ref group) => {
127-
let deps = toml.remove_dependency_group_requirement(&package, group)?;
128-
if deps.is_empty() {
129-
warn_if_present(&package, &toml);
130-
anyhow::bail!(
131-
"The dependency `{package}` could not be found in `dependency-groups`"
132-
);
130+
if group == &*DEV_DEPENDENCIES {
131+
let dev_deps = toml.remove_dev_dependency(&package)?;
132+
let group_deps =
133+
toml.remove_dependency_group_requirement(&package, &DEV_DEPENDENCIES)?;
134+
if dev_deps.is_empty() && group_deps.is_empty() {
135+
warn_if_present(&package, &toml);
136+
anyhow::bail!(
137+
"The dependency `{package}` could not be found in `dev-dependencies` or `dependency-groups.dev`"
138+
);
139+
}
140+
} else {
141+
let deps = toml.remove_dependency_group_requirement(&package, group)?;
142+
if deps.is_empty() {
143+
warn_if_present(&package, &toml);
144+
anyhow::bail!(
145+
"The dependency `{package}` could not be found in `dependency-groups`"
146+
);
147+
}
133148
}
134149
}
135150
}
@@ -262,8 +277,10 @@ fn warn_if_present(name: &PackageName, pyproject: &PyProjectTomlMut) {
262277
"`{name}` is an optional dependency; try calling `uv remove --optional {group}`",
263278
);
264279
}
265-
DependencyType::Group(_) => {
266-
// TODO(zanieb): Once we support `remove --group`, add a warning here.
280+
DependencyType::Group(group) => {
281+
warn_user!(
282+
"`{name}` is in the `{group}` group; try calling `uv remove --group {group}`",
283+
);
267284
}
268285
}
269286
}

0 commit comments

Comments
 (0)