Skip to content
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
20 changes: 20 additions & 0 deletions crates/uv-workspace/src/pyproject_mut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,26 @@ impl PyProjectTomlMut {
Ok(())
}

/// Returns `true` if the `tool.uv.dev-dependencies` table is present.
pub fn has_dev_dependencies(&self) -> bool {
self.doc
.get("tool")
.and_then(Item::as_table)
.and_then(|tool| tool.get("uv"))
.and_then(Item::as_table)
.and_then(|uv| uv.get("dev-dependencies"))
.is_some()
}

/// Returns `true` if the `dependency-groups` table is present and contains the given group.
pub fn has_dependency_group(&self, group: &GroupName) -> bool {
self.doc
.get("dependency-groups")
.and_then(Item::as_table)
.and_then(|groups| groups.get(group.as_ref()))
.is_some()
}

/// Returns all the places in this `pyproject.toml` that contain a dependency with the given
/// name.
///
Expand Down
69 changes: 61 additions & 8 deletions crates/uv/src/commands/project/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use uv_distribution::DistributionDatabase;
use uv_distribution_types::{Index, IndexName, UnresolvedRequirement, VersionId};
use uv_fs::Simplified;
use uv_git::{GitReference, GIT_STORE};
use uv_normalize::PackageName;
use uv_normalize::{PackageName, DEV_DEPENDENCIES};
use uv_pep508::{ExtraName, Requirement, UnnamedRequirement, VersionOrUrl};
use uv_pypi_types::{redact_credentials, ParsedUrl, RequirementSource, VerbatimParsedUrl};
use uv_python::{
Expand Down Expand Up @@ -203,8 +203,12 @@ pub(crate) async fn add(
DependencyType::Optional(_) => {
bail!("Project is missing a `[project]` table; add a `[project]` table to use optional dependencies, or run `{}` instead", "uv add --dev".green())
}
DependencyType::Group(_) => {
// TODO(charlie): Allow adding to `dependency-groups` in non-`[project]`
// targets, per PEP 735.
bail!("Project is missing a `[project]` table; add a `[project]` table to use `dependency-groups` dependencies, or run `{}` instead", "uv add --dev".green())
}
DependencyType::Dev => (),
DependencyType::Group(_) => (),
}
}

Expand Down Expand Up @@ -463,8 +467,49 @@ pub(crate) async fn add(
_ => source,
};

// Determine the dependency type.
let dependency_type = match &dependency_type {
DependencyType::Dev => {
let existing = toml.find_dependency(&requirement.name, None);
if existing.iter().any(|dependency_type| matches!(dependency_type, DependencyType::Group(group) if group == &*DEV_DEPENDENCIES)) {
// If the dependency already exists in `dependency-groups.dev`, use that.
DependencyType::Group(DEV_DEPENDENCIES.clone())
} else if existing.iter().any(|dependency_type| matches!(dependency_type, DependencyType::Dev)) {
// If the dependency already exists in `dev-dependencies`, use that.
DependencyType::Dev
} else if target.as_project().is_some_and(uv_workspace::VirtualProject::is_non_project) {
// TODO(charlie): Allow adding to `dependency-groups` in non-`[project]` targets.
DependencyType::Dev
} else {
// Otherwise, use `dependency-groups.dev`, unless it would introduce a separate table.
match (toml.has_dev_dependencies(), toml.has_dependency_group(&DEV_DEPENDENCIES)) {
(true, false) => DependencyType::Dev,
(false, true) => DependencyType::Group(DEV_DEPENDENCIES.clone()),
(true, true) => DependencyType::Group(DEV_DEPENDENCIES.clone()),
(false, false) => DependencyType::Group(DEV_DEPENDENCIES.clone()),
}
}
}
DependencyType::Group(group) if group == &*DEV_DEPENDENCIES => {
let existing = toml.find_dependency(&requirement.name, None);
if existing.iter().any(|dependency_type| matches!(dependency_type, DependencyType::Group(group) if group == &*DEV_DEPENDENCIES)) {
// If the dependency already exists in `dependency-groups.dev`, use that.
DependencyType::Group(DEV_DEPENDENCIES.clone())
} else if existing.iter().any(|dependency_type| matches!(dependency_type, DependencyType::Dev)) {
// If the dependency already exists in `dev-dependencies`, use that.
DependencyType::Dev
} else {
// Otherwise, use `dependency-groups.dev`.
DependencyType::Group(DEV_DEPENDENCIES.clone())
}
}
DependencyType::Production => DependencyType::Production,
DependencyType::Optional(extra) => DependencyType::Optional(extra.clone()),
DependencyType::Group(group) => DependencyType::Group(group.clone()),
};

// Update the `pyproject.toml`.
let edit = match dependency_type {
let edit = match &dependency_type {
DependencyType::Production => toml.add_dependency(&requirement, source.as_ref())?,
DependencyType::Dev => toml.add_dev_dependency(&requirement, source.as_ref())?,
DependencyType::Optional(ref extra) => {
Expand All @@ -478,7 +523,7 @@ pub(crate) async fn add(
// If the edit was inserted before the end of the list, update the existing edits.
if let ArrayEdit::Add(index) = &edit {
for edit in &mut edits {
if *edit.dependency_type == dependency_type {
if edit.dependency_type == dependency_type {
match &mut edit.edit {
ArrayEdit::Add(existing) => {
if *existing >= *index {
Expand All @@ -496,7 +541,7 @@ pub(crate) async fn add(
}

edits.push(DependencyEdit {
dependency_type: &dependency_type,
dependency_type,
requirement,
source,
edit,
Expand Down Expand Up @@ -646,7 +691,7 @@ pub(crate) async fn add(
async fn lock_and_sync(
mut project: VirtualProject,
toml: &mut PyProjectTomlMut,
edits: &[DependencyEdit<'_>],
edits: &[DependencyEdit],
venv: &PythonEnvironment,
state: SharedState,
locked: bool,
Expand Down Expand Up @@ -973,6 +1018,14 @@ enum Target {
}

impl Target {
/// Returns the [`VirtualProject`] for the target, if it is a project.
fn as_project(&self) -> Option<&VirtualProject> {
match self {
Self::Project(project, _) => Some(project),
Self::Script(_, _) => None,
}
}

/// Returns the [`Interpreter`] for the target.
fn interpreter(&self) -> &Interpreter {
match self {
Expand All @@ -983,8 +1036,8 @@ impl Target {
}

#[derive(Debug, Clone)]
struct DependencyEdit<'a> {
dependency_type: &'a DependencyType,
struct DependencyEdit {
dependency_type: DependencyType,
requirement: Requirement,
source: Option<Source>,
edit: ArrayEdit,
Expand Down
39 changes: 28 additions & 11 deletions crates/uv/src/commands/project/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use uv_configuration::{
Concurrency, DevGroupsManifest, EditableMode, ExtrasSpecification, InstallOptions, LowerBound,
};
use uv_fs::Simplified;
use uv_normalize::DEV_DEPENDENCIES;
use uv_pep508::PackageName;
use uv_python::{PythonDownloads, PythonPreference, PythonRequest};
use uv_scripts::Pep723Script;
Expand Down Expand Up @@ -106,11 +107,13 @@ pub(crate) async fn remove(
}
}
DependencyType::Dev => {
let deps = toml.remove_dev_dependency(&package)?;
if deps.is_empty() {
let dev_deps = toml.remove_dev_dependency(&package)?;
let group_deps =
toml.remove_dependency_group_requirement(&package, &DEV_DEPENDENCIES)?;
if dev_deps.is_empty() && group_deps.is_empty() {
warn_if_present(&package, &toml);
anyhow::bail!(
"The dependency `{package}` could not be found in `dev-dependencies`"
"The dependency `{package}` could not be found in `dev-dependencies` or `dependency-groups.dev`"
);
}
}
Expand All @@ -124,12 +127,24 @@ pub(crate) async fn remove(
}
}
DependencyType::Group(ref group) => {
let deps = toml.remove_dependency_group_requirement(&package, group)?;
if deps.is_empty() {
warn_if_present(&package, &toml);
anyhow::bail!(
"The dependency `{package}` could not be found in `dependency-groups`"
);
if group == &*DEV_DEPENDENCIES {
let dev_deps = toml.remove_dev_dependency(&package)?;
let group_deps =
toml.remove_dependency_group_requirement(&package, &DEV_DEPENDENCIES)?;
if dev_deps.is_empty() && group_deps.is_empty() {
warn_if_present(&package, &toml);
anyhow::bail!(
"The dependency `{package}` could not be found in `dev-dependencies` or `dependency-groups.dev`"
);
}
} else {
let deps = toml.remove_dependency_group_requirement(&package, group)?;
if deps.is_empty() {
warn_if_present(&package, &toml);
anyhow::bail!(
"The dependency `{package}` could not be found in `dependency-groups`"
);
}
}
}
}
Expand Down Expand Up @@ -262,8 +277,10 @@ fn warn_if_present(name: &PackageName, pyproject: &PyProjectTomlMut) {
"`{name}` is an optional dependency; try calling `uv remove --optional {group}`",
);
}
DependencyType::Group(_) => {
// TODO(zanieb): Once we support `remove --group`, add a warning here.
DependencyType::Group(group) => {
warn_user!(
"`{name}` is in the `{group}` group; try calling `uv remove --group {group}`",
);
}
}
}
Expand Down
Loading