Skip to content

Commit 8d08a65

Browse files
Add support for default-groups (#8471)
## Summary This PR adds support for `tool.uv.default-groups`, which defaults to `["dev"]` for backwards-compatibility. These represent the groups we sync by default.
1 parent 0d12174 commit 8d08a65

File tree

19 files changed

+480
-172
lines changed

19 files changed

+480
-172
lines changed

crates/uv-configuration/src/dev.rs

Lines changed: 145 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,16 @@ pub enum DevMode {
1515
}
1616

1717
impl DevMode {
18-
/// Iterate over the group names to include.
19-
pub fn iter(&self) -> impl Iterator<Item = &GroupName> {
20-
match self {
21-
Self::Exclude => Either::Left(std::iter::empty()),
22-
Self::Include | Self::Only => Either::Right(std::iter::once(&*DEV_DEPENDENCIES)),
23-
}
24-
}
25-
2618
/// Returns `true` if the specification allows for production dependencies.
2719
pub fn prod(&self) -> bool {
2820
matches!(self, Self::Exclude | Self::Include)
2921
}
3022

23+
/// Returns `true` if the specification only includes development dependencies.
24+
pub fn only(&self) -> bool {
25+
matches!(self, Self::Only)
26+
}
27+
3128
/// Returns the flag that was used to request development dependencies.
3229
pub fn as_flag(&self) -> &'static str {
3330
match self {
@@ -36,9 +33,26 @@ impl DevMode {
3633
Self::Only => "--only-dev",
3734
}
3835
}
36+
37+
/// Iterate over the group names to include.
38+
pub fn iter(&self) -> impl Iterator<Item = &GroupName> {
39+
<&Self as IntoIterator>::into_iter(self)
40+
}
41+
}
42+
43+
impl<'a> IntoIterator for &'a DevMode {
44+
type Item = &'a GroupName;
45+
type IntoIter = Either<std::iter::Empty<&'a GroupName>, std::iter::Once<&'a GroupName>>;
46+
47+
fn into_iter(self) -> Self::IntoIter {
48+
match self {
49+
DevMode::Exclude => Either::Left(std::iter::empty()),
50+
DevMode::Include | DevMode::Only => Either::Right(std::iter::once(&*DEV_DEPENDENCIES)),
51+
}
52+
}
3953
}
4054

41-
#[derive(Debug, Clone)]
55+
#[derive(Default, Debug, Clone)]
4256
pub struct DevGroupsSpecification {
4357
/// Legacy option for `dependency-group.dev` and `tool.uv.dev-dependencies`.
4458
///
@@ -48,37 +62,31 @@ pub struct DevGroupsSpecification {
4862
/// The groups to include.
4963
///
5064
/// Requested via the `--group` and `--only-group` options.
51-
groups: GroupsSpecification,
65+
groups: Option<GroupsSpecification>,
5266
}
5367

5468
#[derive(Debug, Clone)]
5569
pub enum GroupsSpecification {
5670
/// Include dependencies from the specified groups.
5771
Include(Vec<GroupName>),
58-
/// Do not include dependencies from groups.
59-
Exclude,
6072
/// Only include dependencies from the specified groups, exclude all other dependencies.
6173
Only(Vec<GroupName>),
6274
}
6375

6476
impl GroupsSpecification {
65-
/// Returns an [`Iterator`] over the group names to include.
66-
pub fn iter(&self) -> impl Iterator<Item = &GroupName> {
67-
match self {
68-
Self::Exclude => Either::Left(std::iter::empty()),
69-
Self::Include(groups) | Self::Only(groups) => Either::Right(groups.iter()),
70-
}
71-
}
72-
7377
/// Returns `true` if the specification allows for production dependencies.
7478
pub fn prod(&self) -> bool {
75-
matches!(self, Self::Exclude | Self::Include(_))
79+
matches!(self, Self::Include(_))
80+
}
81+
82+
/// Returns `true` if the specification is limited to a select set of groups.
83+
pub fn only(&self) -> bool {
84+
matches!(self, Self::Only(_))
7685
}
7786

7887
/// Returns the option that was used to request the groups, if any.
7988
pub fn as_flag(&self) -> Option<Cow<'_, str>> {
8089
match self {
81-
Self::Exclude => None,
8290
Self::Include(groups) => match groups.as_slice() {
8391
[] => None,
8492
[group] => Some(Cow::Owned(format!("--group {group}"))),
@@ -91,17 +99,27 @@ impl GroupsSpecification {
9199
},
92100
}
93101
}
94-
}
95102

96-
impl DevGroupsSpecification {
97-
/// Returns an [`Iterator`] over the group names to include.
103+
/// Iterate over the group names to include.
98104
pub fn iter(&self) -> impl Iterator<Item = &GroupName> {
99-
match self.dev {
100-
None => Either::Left(self.groups.iter()),
101-
Some(ref dev_mode) => Either::Right(self.groups.iter().chain(dev_mode.iter())),
105+
<&Self as IntoIterator>::into_iter(self)
106+
}
107+
}
108+
109+
impl<'a> IntoIterator for &'a GroupsSpecification {
110+
type Item = &'a GroupName;
111+
type IntoIter = std::slice::Iter<'a, GroupName>;
112+
113+
fn into_iter(self) -> Self::IntoIter {
114+
match self {
115+
GroupsSpecification::Include(groups) | GroupsSpecification::Only(groups) => {
116+
groups.iter()
117+
}
102118
}
103119
}
120+
}
104121

122+
impl DevGroupsSpecification {
105123
/// Determine the [`DevGroupsSpecification`] policy from the command-line arguments.
106124
pub fn from_args(
107125
dev: bool,
@@ -110,7 +128,7 @@ impl DevGroupsSpecification {
110128
group: Vec<GroupName>,
111129
only_group: Vec<GroupName>,
112130
) -> Self {
113-
let dev_mode = if only_dev {
131+
let dev = if only_dev {
114132
Some(DevMode::Only)
115133
} else if no_dev {
116134
Some(DevMode::Exclude)
@@ -121,70 +139,141 @@ impl DevGroupsSpecification {
121139
};
122140

123141
let groups = if !group.is_empty() {
124-
if matches!(dev_mode, Some(DevMode::Only)) {
142+
if matches!(dev, Some(DevMode::Only)) {
125143
unreachable!("cannot specify both `--only-dev` and `--group`")
126144
};
127-
GroupsSpecification::Include(group)
145+
Some(GroupsSpecification::Include(group))
128146
} else if !only_group.is_empty() {
129-
if matches!(dev_mode, Some(DevMode::Include)) {
147+
if matches!(dev, Some(DevMode::Include)) {
130148
unreachable!("cannot specify both `--dev` and `--only-group`")
131149
};
132-
GroupsSpecification::Only(only_group)
150+
Some(GroupsSpecification::Only(only_group))
133151
} else {
134-
GroupsSpecification::Exclude
152+
None
135153
};
136154

137-
Self {
138-
dev: dev_mode,
139-
groups,
140-
}
155+
Self { dev, groups }
141156
}
142157

143158
/// Return a new [`DevGroupsSpecification`] with development dependencies included by default.
144159
///
145160
/// This is appropriate in projects, where the `dev` group is synced by default.
146161
#[must_use]
147-
pub fn with_default_dev(self) -> Self {
148-
match self.dev {
149-
Some(_) => self,
150-
None => match self.groups {
151-
// Only include the default `dev` group if `--only-group` wasn't used
152-
GroupsSpecification::Only(_) => self,
153-
GroupsSpecification::Exclude | GroupsSpecification::Include(_) => Self {
154-
dev: Some(DevMode::Include),
155-
..self
156-
},
157-
},
162+
pub fn with_defaults(self, defaults: Vec<GroupName>) -> DevGroupsManifest {
163+
DevGroupsManifest {
164+
spec: self,
165+
defaults,
158166
}
159167
}
160168

161169
/// Returns `true` if the specification allows for production dependencies.
162170
pub fn prod(&self) -> bool {
163-
(self.dev.is_none() || self.dev.as_ref().is_some_and(DevMode::prod)) && self.groups.prod()
171+
self.dev.as_ref().map_or(true, DevMode::prod)
172+
&& self.groups.as_ref().map_or(true, GroupsSpecification::prod)
164173
}
165174

166-
/// Returns the flag that was used to request development dependencies.
175+
/// Returns `true` if the specification is limited to a select set of groups.
176+
pub fn only(&self) -> bool {
177+
self.dev.as_ref().is_some_and(DevMode::only)
178+
|| self.groups.as_ref().is_some_and(GroupsSpecification::only)
179+
}
180+
181+
/// Returns the flag that was used to request development dependencies, if specified.
167182
pub fn dev_mode(&self) -> Option<&DevMode> {
168183
self.dev.as_ref()
169184
}
170185

171-
/// Returns the list of groups to include.
172-
pub fn groups(&self) -> &GroupsSpecification {
173-
&self.groups
186+
/// Returns the list of groups to include, if specified.
187+
pub fn groups(&self) -> Option<&GroupsSpecification> {
188+
self.groups.as_ref()
189+
}
190+
191+
/// Returns an [`Iterator`] over the group names to include.
192+
pub fn iter(&self) -> impl Iterator<Item = &GroupName> {
193+
<&Self as IntoIterator>::into_iter(self)
194+
}
195+
}
196+
197+
impl<'a> IntoIterator for &'a DevGroupsSpecification {
198+
type Item = &'a GroupName;
199+
type IntoIter = std::iter::Chain<
200+
std::iter::Flatten<std::option::IntoIter<&'a DevMode>>,
201+
std::iter::Flatten<std::option::IntoIter<&'a GroupsSpecification>>,
202+
>;
203+
204+
fn into_iter(self) -> Self::IntoIter {
205+
self.dev
206+
.as_ref()
207+
.into_iter()
208+
.flatten()
209+
.chain(self.groups.as_ref().into_iter().flatten())
174210
}
175211
}
176212

177213
impl From<DevMode> for DevGroupsSpecification {
178214
fn from(dev: DevMode) -> Self {
179215
Self {
180216
dev: Some(dev),
181-
groups: GroupsSpecification::Exclude,
217+
groups: None,
182218
}
183219
}
184220
}
185221

186222
impl From<GroupsSpecification> for DevGroupsSpecification {
187223
fn from(groups: GroupsSpecification) -> Self {
188-
Self { dev: None, groups }
224+
Self {
225+
dev: None,
226+
groups: Some(groups),
227+
}
228+
}
229+
}
230+
231+
/// The manifest of `dependency-groups` to include, taking into account the user-provided
232+
/// [`DevGroupsSpecification`] and the project-specific default groups.
233+
#[derive(Debug, Clone)]
234+
pub struct DevGroupsManifest {
235+
/// The specification for the development dependencies.
236+
pub(crate) spec: DevGroupsSpecification,
237+
/// The default groups to include.
238+
pub(crate) defaults: Vec<GroupName>,
239+
}
240+
241+
impl DevGroupsManifest {
242+
/// Returns a new [`DevGroupsManifest`] with the given default groups.
243+
pub fn from_defaults(defaults: Vec<GroupName>) -> Self {
244+
Self {
245+
spec: DevGroupsSpecification::default(),
246+
defaults,
247+
}
248+
}
249+
250+
/// Returns a new [`DevGroupsManifest`] with the given specification.
251+
pub fn from_spec(spec: DevGroupsSpecification) -> Self {
252+
Self {
253+
spec,
254+
defaults: Vec::new(),
255+
}
256+
}
257+
258+
/// Returns `true` if the specification allows for production dependencies.
259+
pub fn prod(&self) -> bool {
260+
self.spec.prod()
261+
}
262+
263+
/// Returns an [`Iterator`] over the group names to include.
264+
pub fn iter(&self) -> impl Iterator<Item = &GroupName> {
265+
if self.spec.only() {
266+
Either::Left(self.spec.iter())
267+
} else {
268+
Either::Right(
269+
self.spec
270+
.iter()
271+
.chain(self.defaults.iter().filter(|default| {
272+
// 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
275+
})),
276+
)
277+
}
189278
}
190279
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use toml_edit::{value, Array, ArrayOfTables, InlineTable, Item, Table, Value};
1515
use url::Url;
1616

1717
use uv_cache_key::RepositoryUrl;
18-
use uv_configuration::{BuildOptions, DevGroupsSpecification, ExtrasSpecification, InstallOptions};
18+
use uv_configuration::{BuildOptions, DevGroupsManifest, ExtrasSpecification, InstallOptions};
1919
use uv_distribution::DistributionDatabase;
2020
use uv_distribution_filename::{DistExtension, ExtensionError, SourceDistExtension, WheelFilename};
2121
use uv_distribution_types::{
@@ -575,7 +575,7 @@ impl Lock {
575575
marker_env: &ResolverMarkerEnvironment,
576576
tags: &Tags,
577577
extras: &ExtrasSpecification,
578-
dev: &DevGroupsSpecification,
578+
dev: &DevGroupsManifest,
579579
build_options: &BuildOptions,
580580
install_options: &InstallOptions,
581581
) -> Result<Resolution, LockError> {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use petgraph::{Directed, Graph};
1010
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
1111
use url::Url;
1212

13-
use uv_configuration::{DevGroupsSpecification, EditableMode, ExtrasSpecification, InstallOptions};
13+
use uv_configuration::{DevGroupsManifest, EditableMode, ExtrasSpecification, InstallOptions};
1414
use uv_distribution_filename::{DistExtension, SourceDistExtension};
1515
use uv_fs::Simplified;
1616
use uv_git::GitReference;
@@ -43,7 +43,7 @@ impl<'lock> RequirementsTxtExport<'lock> {
4343
lock: &'lock Lock,
4444
root_name: &PackageName,
4545
extras: &ExtrasSpecification,
46-
dev: &DevGroupsSpecification,
46+
dev: &DevGroupsManifest,
4747
editable: EditableMode,
4848
hashes: bool,
4949
install_options: &'lock InstallOptions,

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::collections::BTreeSet;
44
use itertools::Itertools;
55
use rustc_hash::{FxHashMap, FxHashSet};
66

7-
use uv_configuration::DevGroupsSpecification;
7+
use uv_configuration::DevGroupsManifest;
88
use uv_normalize::{ExtraName, GroupName, PackageName};
99
use uv_pypi_types::ResolverMarkerEnvironment;
1010

@@ -26,7 +26,7 @@ pub struct TreeDisplay<'env> {
2626
/// Maximum display depth of the dependency tree.
2727
depth: usize,
2828
/// Whether to include development dependencies in the display.
29-
dev: DevGroupsSpecification,
29+
dev: DevGroupsManifest,
3030
/// Prune the given packages from the display of the dependency tree.
3131
prune: Vec<PackageName>,
3232
/// Display only the specified packages.
@@ -43,7 +43,7 @@ impl<'env> TreeDisplay<'env> {
4343
depth: usize,
4444
prune: Vec<PackageName>,
4545
packages: Vec<PackageName>,
46-
dev: DevGroupsSpecification,
46+
dev: DevGroupsManifest,
4747
no_dedupe: bool,
4848
invert: bool,
4949
) -> Self {

0 commit comments

Comments
 (0)