Skip to content

Commit a6b83fa

Browse files
committed
Add --no-group support to CLI (#8477)
## Summary Now that `default-groups` can include more than just `"dev"`, it makes sense to allow users to remove groups with `--no-group`.
1 parent 7cba853 commit a6b83fa

File tree

7 files changed

+278
-26
lines changed

7 files changed

+278
-26
lines changed

crates/uv-cli/src/lib.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2617,6 +2617,12 @@ pub struct RunArgs {
26172617
#[arg(long, conflicts_with("only_group"))]
26182618
pub group: Vec<GroupName>,
26192619

2620+
/// Exclude dependencies from the specified local dependency group.
2621+
///
2622+
/// May be provided multiple times.
2623+
#[arg(long)]
2624+
pub no_group: Vec<GroupName>,
2625+
26202626
/// Only include dependencies from the specified local dependency group.
26212627
///
26222628
/// May be provided multiple times.
@@ -2808,6 +2814,12 @@ pub struct SyncArgs {
28082814
#[arg(long, conflicts_with("only_group"))]
28092815
pub group: Vec<GroupName>,
28102816

2817+
/// Exclude dependencies from the specified local dependency group.
2818+
///
2819+
/// May be provided multiple times.
2820+
#[arg(long)]
2821+
pub no_group: Vec<GroupName>,
2822+
28112823
/// Only include dependencies from the specified local dependency group.
28122824
///
28132825
/// May be provided multiple times.
@@ -3205,6 +3217,12 @@ pub struct TreeArgs {
32053217
#[arg(long, conflicts_with("only_group"))]
32063218
pub group: Vec<GroupName>,
32073219

3220+
/// Exclude dependencies from the specified local dependency group.
3221+
///
3222+
/// May be provided multiple times.
3223+
#[arg(long)]
3224+
pub no_group: Vec<GroupName>,
3225+
32083226
/// Only include dependencies from the specified local dependency group.
32093227
///
32103228
/// May be provided multiple times.
@@ -3320,6 +3338,12 @@ pub struct ExportArgs {
33203338
#[arg(long, conflicts_with("only_group"))]
33213339
pub group: Vec<GroupName>,
33223340

3341+
/// Exclude dependencies from the specified local dependency group.
3342+
///
3343+
/// May be provided multiple times.
3344+
#[arg(long)]
3345+
pub no_group: Vec<GroupName>,
3346+
33233347
/// Only include dependencies from the specified local dependency group.
33243348
///
33253349
/// May be provided multiple times.

crates/uv-configuration/src/dev.rs

Lines changed: 95 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -68,38 +68,76 @@ pub struct DevGroupsSpecification {
6868
#[derive(Debug, Clone)]
6969
pub enum GroupsSpecification {
7070
/// Include dependencies from the specified groups.
71-
Include(Vec<GroupName>),
71+
///
72+
/// The `include` list is guaranteed to omit groups in the `exclude` list (i.e., they have an
73+
/// empty intersection).
74+
Include {
75+
include: Vec<GroupName>,
76+
exclude: Vec<GroupName>,
77+
},
7278
/// Only include dependencies from the specified groups, exclude all other dependencies.
73-
Only(Vec<GroupName>),
79+
///
80+
/// The `include` list is guaranteed to omit groups in the `exclude` list (i.e., they have an
81+
/// empty intersection).
82+
Only {
83+
include: Vec<GroupName>,
84+
exclude: Vec<GroupName>,
85+
},
7486
}
7587

7688
impl GroupsSpecification {
89+
/// Create a [`GroupsSpecification`] that includes the given group.
90+
pub fn from_group(group: GroupName) -> Self {
91+
Self::Include {
92+
include: vec![group],
93+
exclude: Vec::new(),
94+
}
95+
}
96+
7797
/// Returns `true` if the specification allows for production dependencies.
7898
pub fn prod(&self) -> bool {
79-
matches!(self, Self::Include(_))
99+
matches!(self, Self::Include { .. })
80100
}
81101

82102
/// Returns `true` if the specification is limited to a select set of groups.
83103
pub fn only(&self) -> bool {
84-
matches!(self, Self::Only(_))
104+
matches!(self, Self::Only { .. })
85105
}
86106

87107
/// Returns the option that was used to request the groups, if any.
88108
pub fn as_flag(&self) -> Option<Cow<'_, str>> {
89109
match self {
90-
Self::Include(groups) => match groups.as_slice() {
91-
[] => None,
110+
Self::Include { include, exclude } => match include.as_slice() {
111+
[] => match exclude.as_slice() {
112+
[] => None,
113+
[group] => Some(Cow::Owned(format!("--no-group {group}"))),
114+
[..] => Some(Cow::Borrowed("--no-group")),
115+
},
92116
[group] => Some(Cow::Owned(format!("--group {group}"))),
93117
[..] => Some(Cow::Borrowed("--group")),
94118
},
95-
Self::Only(groups) => match groups.as_slice() {
96-
[] => None,
119+
Self::Only { include, exclude } => match include.as_slice() {
120+
[] => match exclude.as_slice() {
121+
[] => None,
122+
[group] => Some(Cow::Owned(format!("--no-group {group}"))),
123+
[..] => Some(Cow::Borrowed("--no-group")),
124+
},
97125
[group] => Some(Cow::Owned(format!("--only-group {group}"))),
98126
[..] => Some(Cow::Borrowed("--only-group")),
99127
},
100128
}
101129
}
102130

131+
/// Iterate over all groups referenced in the [`DevGroupsSpecification`].
132+
pub fn names(&self) -> impl Iterator<Item = &GroupName> {
133+
match self {
134+
GroupsSpecification::Include { include, exclude }
135+
| GroupsSpecification::Only { include, exclude } => {
136+
include.iter().chain(exclude.iter())
137+
}
138+
}
139+
}
140+
103141
/// Iterate over the group names to include.
104142
pub fn iter(&self) -> impl Iterator<Item = &GroupName> {
105143
<&Self as IntoIterator>::into_iter(self)
@@ -112,9 +150,14 @@ impl<'a> IntoIterator for &'a GroupsSpecification {
112150

113151
fn into_iter(self) -> Self::IntoIter {
114152
match self {
115-
GroupsSpecification::Include(groups) | GroupsSpecification::Only(groups) => {
116-
groups.iter()
153+
GroupsSpecification::Include {
154+
include,
155+
exclude: _,
117156
}
157+
| GroupsSpecification::Only {
158+
include,
159+
exclude: _,
160+
} => include.iter(),
118161
}
119162
}
120163
}
@@ -125,8 +168,9 @@ impl DevGroupsSpecification {
125168
dev: bool,
126169
no_dev: bool,
127170
only_dev: bool,
128-
group: Vec<GroupName>,
129-
only_group: Vec<GroupName>,
171+
mut group: Vec<GroupName>,
172+
no_group: Vec<GroupName>,
173+
mut only_group: Vec<GroupName>,
130174
) -> Self {
131175
let dev = if only_dev {
132176
Some(DevMode::Only)
@@ -142,12 +186,31 @@ impl DevGroupsSpecification {
142186
if matches!(dev, Some(DevMode::Only)) {
143187
unreachable!("cannot specify both `--only-dev` and `--group`")
144188
};
145-
Some(GroupsSpecification::Include(group))
189+
190+
// Ensure that `--no-group` and `--group` are mutually exclusive.
191+
group.retain(|group| !no_group.contains(group));
192+
193+
Some(GroupsSpecification::Include {
194+
include: group,
195+
exclude: no_group,
196+
})
146197
} else if !only_group.is_empty() {
147198
if matches!(dev, Some(DevMode::Include)) {
148199
unreachable!("cannot specify both `--dev` and `--only-group`")
149200
};
150-
Some(GroupsSpecification::Only(only_group))
201+
202+
// Ensure that `--no-group` and `--only-group` are mutually exclusive.
203+
only_group.retain(|group| !no_group.contains(group));
204+
205+
Some(GroupsSpecification::Only {
206+
include: only_group,
207+
exclude: no_group,
208+
})
209+
} else if !no_group.is_empty() {
210+
Some(GroupsSpecification::Include {
211+
include: Vec::new(),
212+
exclude: no_group,
213+
})
151214
} else {
152215
None
153216
};
@@ -270,8 +333,24 @@ impl DevGroupsManifest {
270333
.iter()
271334
.chain(self.defaults.iter().filter(|default| {
272335
// If `--no-dev` was provided, exclude the `dev` group from the list of defaults.
273-
!matches!(self.spec.dev_mode(), Some(DevMode::Exclude))
274-
|| *default != &*DEV_DEPENDENCIES
336+
if matches!(self.spec.dev_mode(), Some(DevMode::Exclude)) {
337+
if *default == &*DEV_DEPENDENCIES {
338+
return false;
339+
};
340+
}
341+
342+
// If `--no-group` was provided, exclude the group from the list of defaults.
343+
if let Some(GroupsSpecification::Include {
344+
include: _,
345+
exclude,
346+
}) = self.spec.groups()
347+
{
348+
if exclude.contains(default) {
349+
return false;
350+
}
351+
}
352+
353+
true
275354
})),
276355
)
277356
}

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -827,9 +827,7 @@ async fn lock_and_sync(
827827
DependencyType::Group(ref group_name) => {
828828
let extras = ExtrasSpecification::None;
829829
let dev =
830-
DevGroupsSpecification::from(GroupsSpecification::Include(
831-
vec![group_name.clone()],
832-
));
830+
DevGroupsSpecification::from(GroupsSpecification::from_group(group_name.clone()));
833831
(extras, dev)
834832
}
835833
};

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ use tracing::debug;
88
use uv_cache::Cache;
99
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
1010
use uv_configuration::{
11-
Concurrency, Constraints, DevGroupsSpecification, ExtrasSpecification, LowerBound, Reinstall,
12-
Upgrade,
11+
Concurrency, Constraints, DevGroupsSpecification, ExtrasSpecification, GroupsSpecification,
12+
LowerBound, Reinstall, Upgrade,
1313
};
1414
use uv_dispatch::BuildDispatch;
1515
use uv_distribution::DistributionDatabase;
@@ -1370,7 +1370,11 @@ pub(crate) fn validate_dependency_groups(
13701370
pyproject_toml: &PyProjectToml,
13711371
dev: &DevGroupsSpecification,
13721372
) -> Result<(), ProjectError> {
1373-
for group in dev.groups().into_iter().flatten() {
1373+
for group in dev
1374+
.groups()
1375+
.into_iter()
1376+
.flat_map(GroupsSpecification::names)
1377+
{
13741378
if !pyproject_toml
13751379
.dependency_groups
13761380
.as_ref()

crates/uv/src/settings.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ impl RunSettings {
253253
dev,
254254
no_dev,
255255
group,
256+
no_group,
256257
only_group,
257258
module: _,
258259
only_dev,
@@ -282,7 +283,9 @@ impl RunSettings {
282283
flag(all_extras, no_all_extras).unwrap_or_default(),
283284
extra.unwrap_or_default(),
284285
),
285-
dev: DevGroupsSpecification::from_args(dev, no_dev, only_dev, group, only_group),
286+
dev: DevGroupsSpecification::from_args(
287+
dev, no_dev, only_dev, group, no_group, only_group,
288+
),
286289
editable: EditableMode::from_args(no_editable),
287290
with,
288291
with_editable,
@@ -718,6 +721,7 @@ impl SyncSettings {
718721
only_dev,
719722
group,
720723
only_group,
724+
no_group,
721725
no_editable,
722726
inexact,
723727
exact,
@@ -745,7 +749,9 @@ impl SyncSettings {
745749
flag(all_extras, no_all_extras).unwrap_or_default(),
746750
extra.unwrap_or_default(),
747751
),
748-
dev: DevGroupsSpecification::from_args(dev, no_dev, only_dev, group, only_group),
752+
dev: DevGroupsSpecification::from_args(
753+
dev, no_dev, only_dev, group, no_group, only_group,
754+
),
749755
editable: EditableMode::from_args(no_editable),
750756
install_options: InstallOptions::new(
751757
no_install_project,
@@ -1028,6 +1034,7 @@ impl TreeSettings {
10281034
only_dev,
10291035
no_dev,
10301036
group,
1037+
no_group,
10311038
only_group,
10321039
locked,
10331040
frozen,
@@ -1039,7 +1046,9 @@ impl TreeSettings {
10391046
} = args;
10401047

10411048
Self {
1042-
dev: DevGroupsSpecification::from_args(dev, no_dev, only_dev, group, only_group),
1049+
dev: DevGroupsSpecification::from_args(
1050+
dev, no_dev, only_dev, group, no_group, only_group,
1051+
),
10431052
locked,
10441053
frozen,
10451054
universal,
@@ -1090,6 +1099,7 @@ impl ExportSettings {
10901099
no_dev,
10911100
only_dev,
10921101
group,
1102+
no_group,
10931103
only_group,
10941104
header,
10951105
no_header,
@@ -1115,7 +1125,9 @@ impl ExportSettings {
11151125
flag(all_extras, no_all_extras).unwrap_or_default(),
11161126
extra.unwrap_or_default(),
11171127
),
1118-
dev: DevGroupsSpecification::from_args(dev, no_dev, only_dev, group, only_group),
1128+
dev: DevGroupsSpecification::from_args(
1129+
dev, no_dev, only_dev, group, no_group, only_group,
1130+
),
11191131
editable: EditableMode::from_args(no_editable),
11201132
hashes: flag(hashes, no_hashes).unwrap_or(true),
11211133
install_options: InstallOptions::new(

0 commit comments

Comments
 (0)