Skip to content

Commit 17bf252

Browse files
committed
Add --group support to uv add and uv remove
1 parent dac60e0 commit 17bf252

File tree

17 files changed

+268
-106
lines changed

17 files changed

+268
-106
lines changed

crates/uv-cli/src/lib.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use uv_configuration::{
1414
TargetTriple, TrustedHost, TrustedPublishing, VersionControlSystem,
1515
};
1616
use uv_distribution_types::{FlatIndexLocation, IndexUrl};
17-
use uv_normalize::{ExtraName, PackageName};
17+
use uv_normalize::{ExtraName, GroupName, PackageName};
1818
use uv_pep508::Requirement;
1919
use uv_pypi_types::VerbatimParsedUrl;
2020
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
@@ -2877,7 +2877,7 @@ pub struct AddArgs {
28772877
pub requirements: Vec<PathBuf>,
28782878

28792879
/// Add the requirements as development dependencies.
2880-
#[arg(long, conflicts_with("optional"))]
2880+
#[arg(long, conflicts_with("optional"), conflicts_with("group"))]
28812881
pub dev: bool,
28822882

28832883
/// Add the requirements to the specified optional dependency group.
@@ -2887,9 +2887,15 @@ pub struct AddArgs {
28872887
///
28882888
/// To enable an optional dependency group for this requirement instead, see
28892889
/// `--extra`.
2890-
#[arg(long, conflicts_with("dev"))]
2890+
#[arg(long, conflicts_with("dev"), conflicts_with("group"))]
28912891
pub optional: Option<ExtraName>,
28922892

2893+
/// Add the requirements to the specified local dependency group.
2894+
///
2895+
/// These requirements will not be included in the published metadata for the project.
2896+
#[arg(long, conflicts_with("dev"), conflicts_with("optional"))]
2897+
pub group: Option<GroupName>,
2898+
28932899
/// Add the requirements as editable.
28942900
#[arg(long, overrides_with = "no_editable")]
28952901
pub editable: bool,
@@ -2995,13 +3001,17 @@ pub struct RemoveArgs {
29953001
pub packages: Vec<PackageName>,
29963002

29973003
/// Remove the packages from the development dependencies.
2998-
#[arg(long, conflicts_with("optional"))]
3004+
#[arg(long, conflicts_with("optional"), conflicts_with("group"))]
29993005
pub dev: bool,
30003006

30013007
/// Remove the packages from the specified optional dependency group.
3002-
#[arg(long, conflicts_with("dev"))]
3008+
#[arg(long, conflicts_with("dev"), conflicts_with("group"))]
30033009
pub optional: Option<ExtraName>,
30043010

3011+
/// Remove the packages from the specified local dependency group.
3012+
#[arg(long, conflicts_with("dev"), conflicts_with("optional"))]
3013+
pub group: Option<GroupName>,
3014+
30053015
/// Avoid syncing the virtual environment after re-locking the project.
30063016
#[arg(long, env = "UV_NO_SYNC", value_parser = clap::builder::BoolishValueParser::new(), conflicts_with = "frozen")]
30073017
pub no_sync: bool,

crates/uv-configuration/src/dev.rs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use either::Either;
2-
use uv_normalize::GroupName;
2+
use uv_normalize::{GroupName, DEV_DEPENDENCIES};
33

44
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
55
pub enum DevMode {
@@ -27,17 +27,17 @@ impl DevMode {
2727
}
2828
}
2929

30-
#[derive(Debug, Copy, Clone)]
31-
pub enum DevSpecification<'group> {
30+
#[derive(Debug, Clone)]
31+
pub enum DevSpecification {
3232
/// Include dev dependencies from the specified group.
33-
Include(&'group [GroupName]),
33+
Include(Vec<GroupName>),
3434
/// Do not include dev dependencies.
3535
Exclude,
36-
/// Include dev dependencies from the specified group, and exclude all non-dev dependencies.
37-
Only(&'group [GroupName]),
36+
/// Include dev dependencies from the specified groups, and exclude all non-dev dependencies.
37+
Only(Vec<GroupName>),
3838
}
3939

40-
impl<'group> DevSpecification<'group> {
40+
impl DevSpecification {
4141
/// Returns an [`Iterator`] over the group names to include.
4242
pub fn iter(&self) -> impl Iterator<Item = &GroupName> {
4343
match self {
@@ -51,3 +51,13 @@ impl<'group> DevSpecification<'group> {
5151
matches!(self, Self::Exclude | Self::Include(_))
5252
}
5353
}
54+
55+
impl From<DevMode> for DevSpecification {
56+
fn from(mode: DevMode) -> Self {
57+
match mode {
58+
DevMode::Include => Self::Include(vec![DEV_DEPENDENCIES.clone()]),
59+
DevMode::Exclude => Self::Exclude,
60+
DevMode::Only => Self::Only(vec![DEV_DEPENDENCIES.clone()]),
61+
}
62+
}
63+
}

crates/uv-distribution/src/metadata/mod.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ use thiserror::Error;
66
use uv_configuration::SourceStrategy;
77
use uv_normalize::{ExtraName, GroupName, PackageName};
88
use uv_pep440::{Version, VersionSpecifiers};
9-
use uv_pypi_types::{HashDigest, ResolutionMetadata};
9+
use uv_pep508::Pep508Error;
10+
use uv_pypi_types::{HashDigest, ResolutionMetadata, VerbatimParsedUrl};
1011
use uv_workspace::WorkspaceError;
1112

1213
pub use crate::metadata::lowering::LoweredRequirement;
@@ -22,8 +23,12 @@ pub enum MetadataError {
2223
Workspace(#[from] WorkspaceError),
2324
#[error("Failed to parse entry for: `{0}`")]
2425
LoweringError(PackageName, #[source] LoweringError),
25-
#[error(transparent)]
26-
Lower(#[from] LoweringError),
26+
#[error("Failed to parse entry for: `{0}`")]
27+
GroupRequirementError(
28+
GroupName,
29+
String,
30+
#[source] Box<Pep508Error<VerbatimParsedUrl>>,
31+
),
2732
}
2833

2934
#[derive(Debug, Clone)]

crates/uv-distribution/src/metadata/requires_dist.rs

Lines changed: 74 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ use crate::Metadata;
33

44
use std::collections::BTreeMap;
55
use std::path::Path;
6+
use std::str::FromStr;
67
use uv_configuration::SourceStrategy;
78
use uv_normalize::{ExtraName, GroupName, PackageName, DEV_DEPENDENCIES};
9+
use uv_pypi_types::VerbatimParsedUrl;
810
use uv_workspace::pyproject::ToolUvSources;
911
use uv_workspace::{DiscoveryOptions, ProjectWorkspace};
1012

@@ -72,7 +74,7 @@ impl RequiresDist {
7274
};
7375

7476
let dev_dependencies = {
75-
let dev_dependencies = project_workspace
77+
let dev_dependencies: Vec<_> = project_workspace
7678
.current_project()
7779
.pyproject_toml()
7880
.tool
@@ -81,36 +83,77 @@ impl RequiresDist {
8183
.and_then(|uv| uv.dev_dependencies.as_ref())
8284
.into_iter()
8385
.flatten()
84-
.cloned();
85-
let dev_dependencies = match source_strategy {
86-
SourceStrategy::Enabled => dev_dependencies
87-
.flat_map(|requirement| {
88-
let requirement_name = requirement.name.clone();
89-
LoweredRequirement::from_requirement(
90-
requirement,
91-
&metadata.name,
92-
project_workspace.project_root(),
93-
sources,
94-
project_workspace.workspace(),
95-
)
96-
.map(move |requirement| match requirement {
97-
Ok(requirement) => Ok(requirement.into_inner()),
98-
Err(err) => {
99-
Err(MetadataError::LoweringError(requirement_name.clone(), err))
100-
}
101-
})
102-
})
103-
.collect::<Result<Vec<_>, _>>()?,
104-
SourceStrategy::Disabled => dev_dependencies
105-
.into_iter()
106-
.map(uv_pypi_types::Requirement::from)
107-
.collect(),
108-
};
109-
if dev_dependencies.is_empty() {
110-
BTreeMap::default()
111-
} else {
112-
BTreeMap::from([(DEV_DEPENDENCIES.clone(), dev_dependencies)])
113-
}
86+
.cloned()
87+
.collect();
88+
89+
let dependency_groups = project_workspace
90+
.current_project()
91+
.pyproject_toml()
92+
.dependency_groups
93+
.iter()
94+
.flatten()
95+
.map(|(name, requirements)| {
96+
(
97+
name.clone(),
98+
requirements
99+
.iter()
100+
.map(|requirement| {
101+
match uv_pep508::Requirement::<VerbatimParsedUrl>::from_str(
102+
requirement,
103+
) {
104+
Ok(requirement) => Ok(requirement),
105+
Err(err) => Err(MetadataError::GroupRequirementError(
106+
name.clone(),
107+
requirement.clone(),
108+
Box::new(err),
109+
)),
110+
}
111+
})
112+
.collect::<Result<Vec<_>, _>>(),
113+
)
114+
})
115+
.chain(std::iter::once((
116+
DEV_DEPENDENCIES.clone(),
117+
Ok(dev_dependencies),
118+
)))
119+
.map(|(name, requirements)| {
120+
let requirements = match requirements {
121+
Ok(requirements) => match source_strategy {
122+
SourceStrategy::Enabled => requirements
123+
.into_iter()
124+
.flat_map(|requirement| {
125+
let requirement_name = requirement.name.clone();
126+
LoweredRequirement::from_requirement(
127+
requirement,
128+
&metadata.name,
129+
project_workspace.project_root(),
130+
sources,
131+
project_workspace.workspace(),
132+
)
133+
.map(move |requirement| {
134+
match requirement {
135+
Ok(requirement) => Ok(requirement.into_inner()),
136+
Err(err) => Err(MetadataError::LoweringError(
137+
requirement_name.clone(),
138+
err,
139+
)),
140+
}
141+
})
142+
})
143+
.collect::<Result<Vec<_>, _>>(),
144+
SourceStrategy::Disabled => Ok(requirements
145+
.into_iter()
146+
.map(uv_pypi_types::Requirement::from)
147+
.collect()),
148+
},
149+
Err(err) => Err(err),
150+
};
151+
// TODO(zanieb): Rewrite this to raise the error correctly
152+
(name, requirements.unwrap())
153+
})
154+
.collect::<BTreeMap<_, _>>();
155+
156+
dependency_groups
114157
};
115158

116159
let requires_dist = metadata.requires_dist.into_iter();

crates/uv-normalize/src/group_name.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::fmt::{Display, Formatter};
33
use std::str::FromStr;
44
use std::sync::LazyLock;
55

6-
use serde::{Deserialize, Deserializer};
6+
use serde::{Deserialize, Deserializer, Serialize, Serializer};
77

88
use crate::{validate_and_normalize_owned, validate_and_normalize_ref, InvalidNameError};
99

@@ -41,6 +41,15 @@ impl<'de> Deserialize<'de> for GroupName {
4141
}
4242
}
4343

44+
impl Serialize for GroupName {
45+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
46+
where
47+
S: Serializer,
48+
{
49+
self.0.serialize(serializer)
50+
}
51+
}
52+
4453
impl Display for GroupName {
4554
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
4655
self.0.fmt(f)

crates/uv-resolver/src/lock/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,7 @@ impl Lock {
551551
marker_env: &ResolverMarkerEnvironment,
552552
tags: &Tags,
553553
extras: &ExtrasSpecification,
554-
dev: DevSpecification<'_>,
554+
dev: &DevSpecification,
555555
build_options: &BuildOptions,
556556
install_options: &InstallOptions,
557557
) -> Result<Resolution, LockError> {

crates/uv-resolver/src/lock/requirements_txt.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ impl<'lock> RequirementsTxtExport<'lock> {
4343
lock: &'lock Lock,
4444
root_name: &PackageName,
4545
extras: &ExtrasSpecification,
46-
dev: DevSpecification<'_>,
46+
dev: &DevSpecification,
4747
editable: EditableMode,
4848
hashes: bool,
4949
install_options: &'lock InstallOptions,

crates/uv-workspace/src/pyproject.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use url::Url;
1919
use uv_fs::{relative_to, PortablePathBuf};
2020
use uv_git::GitReference;
2121
use uv_macros::OptionsMetadata;
22-
use uv_normalize::{ExtraName, PackageName};
22+
use uv_normalize::{ExtraName, GroupName, PackageName};
2323
use uv_pep440::{Version, VersionSpecifiers};
2424
use uv_pep508::MarkerTree;
2525
use uv_pypi_types::{RequirementSource, SupportedEnvironments, VerbatimParsedUrl};
@@ -44,7 +44,7 @@ pub struct PyProjectToml {
4444
/// Tool-specific metadata.
4545
pub tool: Option<Tool>,
4646
/// Non-project dependency groups, as defined in PEP 735.
47-
pub dependency_groups: Option<BTreeMap<ExtraName, Vec<String>>>,
47+
pub dependency_groups: Option<BTreeMap<GroupName, Vec<String>>>,
4848
/// The raw unserialized document.
4949
#[serde(skip)]
5050
pub raw: String,
@@ -1056,7 +1056,7 @@ pub enum DependencyType {
10561056
/// A dependency in `project.optional-dependencies.{0}`.
10571057
Optional(ExtraName),
10581058
/// A dependency in `dependency-groups.{0}`.
1059-
Group(ExtraName),
1059+
Group(GroupName),
10601060
}
10611061

10621062
/// <https://github.com/serde-rs/serde/issues/1316#issue-332908452>

0 commit comments

Comments
 (0)