Skip to content

Commit e485dfd

Browse files
feat: add support for --no-extra flag and setting (#9387)
<!-- Thank you for contributing to uv! To help us review effectively, please ensure that: - The pull request includes a summary of the change. - The title is descriptive and concise. - Relevant issues are referenced where applicable. --> ## Summary Resolves #9333 This pull request introduces support for the `--no-extra` command-line flag and the corresponding `no-extra` UV setting. ### Behavior - When `--all-extras` is supplied, the specified extras in `--no-extra` will be excluded from the installation. - If `--all-extras` is not supplied, `--no-extra` has no effect and is safely ignored. ## Test Plan Since `ExtrasSpecification::from_args` and `ExtrasSpecification::extra_names` are the most important parts in the implementation, I added the following tests in the `uv-configuration/src/extras.rs` module: - **`test_no_extra_full`**: Verifies behavior when `no_extra` includes the entire list of extras. - **`test_no_extra_partial`**: Tests partial exclusion, ensuring only specified extras are excluded. - **`test_no_extra_empty`**: Confirms that no extras are excluded if `no_extra` is empty. - **`test_no_extra_excessive`**: Ensures the implementation ignores `no_extra` values that don't match any available extras. - **`test_no_extra_without_all_extras`**: Validates that `no_extra` has no effect when `--all-extras` is not supplied. - **`test_no_extra_without_package_extras`**: Confirms correct behavior when no extras are available in the package. - **`test_no_extra_duplicates`**: Verifies that duplicate entries in `pkg_extras` or `no_extra` do not cause errors. --------- Co-authored-by: Charlie Marsh <[email protected]>
1 parent c63616c commit e485dfd

File tree

12 files changed

+297
-32
lines changed

12 files changed

+297
-32
lines changed

crates/uv-cli/src/lib.rs

+18
Original file line numberDiff line numberDiff line change
@@ -2599,6 +2599,12 @@ pub struct RunArgs {
25992599
#[arg(long, conflicts_with = "extra")]
26002600
pub all_extras: bool,
26012601

2602+
/// Exclude the specified optional dependencies, if `--all-extras` is supplied.
2603+
///
2604+
/// May be provided multiple times.
2605+
#[arg(long)]
2606+
pub no_extra: Vec<ExtraName>,
2607+
26022608
#[arg(long, overrides_with("all_extras"), hide = true)]
26032609
pub no_all_extras: bool,
26042610

@@ -2836,6 +2842,12 @@ pub struct SyncArgs {
28362842
#[arg(long, conflicts_with = "extra")]
28372843
pub all_extras: bool,
28382844

2845+
/// Exclude the specified optional dependencies, if `--all-extras` is supplied.
2846+
///
2847+
/// May be provided multiple times.
2848+
#[arg(long)]
2849+
pub no_extra: Vec<ExtraName>,
2850+
28392851
#[arg(long, overrides_with("all_extras"), hide = true)]
28402852
pub no_all_extras: bool,
28412853

@@ -3418,6 +3430,12 @@ pub struct ExportArgs {
34183430
#[arg(long, conflicts_with = "extra")]
34193431
pub all_extras: bool,
34203432

3433+
/// Exclude the specified optional dependencies, if `--all-extras` is supplied.
3434+
///
3435+
/// May be provided multiple times.
3436+
#[arg(long)]
3437+
pub no_extra: Vec<ExtraName>,
3438+
34213439
#[arg(long, overrides_with("all_extras"), hide = true)]
34223440
pub no_all_extras: bool,
34233441

crates/uv-configuration/src/extras.rs

+141-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use rustc_hash::FxHashSet;
12
use uv_normalize::ExtraName;
23

34
#[derive(Debug, Default, Clone)]
@@ -6,16 +7,25 @@ pub enum ExtrasSpecification {
67
None,
78
All,
89
Some(Vec<ExtraName>),
10+
Exclude(FxHashSet<ExtraName>),
911
}
1012

1113
impl ExtrasSpecification {
1214
/// Determine the extras specification to use based on the command-line arguments.
13-
pub fn from_args(all_extras: bool, extra: Vec<ExtraName>) -> Self {
14-
if all_extras {
15+
pub fn from_args(
16+
all_extras: bool,
17+
no_extra: Vec<ExtraName>,
18+
mut extra: Vec<ExtraName>,
19+
) -> Self {
20+
if all_extras && !no_extra.is_empty() {
21+
ExtrasSpecification::Exclude(FxHashSet::from_iter(no_extra))
22+
} else if all_extras {
1523
ExtrasSpecification::All
1624
} else if extra.is_empty() {
1725
ExtrasSpecification::None
1826
} else {
27+
// If a package is included in both `no_extra` and `extra`, it should be excluded.
28+
extra.retain(|name| !no_extra.contains(name));
1929
ExtrasSpecification::Some(extra)
2030
}
2131
}
@@ -26,10 +36,139 @@ impl ExtrasSpecification {
2636
ExtrasSpecification::All => true,
2737
ExtrasSpecification::None => false,
2838
ExtrasSpecification::Some(extras) => extras.contains(name),
39+
ExtrasSpecification::Exclude(excluded) => !excluded.contains(name),
2940
}
3041
}
3142

3243
pub fn is_empty(&self) -> bool {
3344
matches!(self, ExtrasSpecification::None)
3445
}
46+
47+
pub fn extra_names<'a, Names>(&'a self, all_names: Names) -> ExtrasIter<'a, Names>
48+
where
49+
Names: Iterator<Item = &'a ExtraName>,
50+
{
51+
match self {
52+
ExtrasSpecification::All => ExtrasIter::All(all_names),
53+
ExtrasSpecification::None => ExtrasIter::None,
54+
ExtrasSpecification::Some(extras) => ExtrasIter::Some(extras.iter()),
55+
ExtrasSpecification::Exclude(excluded) => ExtrasIter::Exclude(all_names, excluded),
56+
}
57+
}
58+
}
59+
60+
/// An iterator over the extra names to include.
61+
#[derive(Debug)]
62+
pub enum ExtrasIter<'a, Names: Iterator<Item = &'a ExtraName>> {
63+
None,
64+
All(Names),
65+
Some(std::slice::Iter<'a, ExtraName>),
66+
Exclude(Names, &'a FxHashSet<ExtraName>),
67+
}
68+
69+
impl<'a, Names: Iterator<Item = &'a ExtraName>> Iterator for ExtrasIter<'a, Names> {
70+
type Item = &'a ExtraName;
71+
72+
fn next(&mut self) -> Option<Self::Item> {
73+
match self {
74+
Self::All(names) => names.next(),
75+
Self::None => None,
76+
Self::Some(extras) => extras.next(),
77+
Self::Exclude(names, excluded) => {
78+
for name in names.by_ref() {
79+
if !excluded.contains(name) {
80+
return Some(name);
81+
}
82+
}
83+
None
84+
}
85+
}
86+
}
87+
}
88+
89+
#[cfg(test)]
90+
mod tests {
91+
use super::*;
92+
93+
macro_rules! extras {
94+
() => (
95+
Vec::new()
96+
);
97+
($($x:expr),+ $(,)?) => (
98+
vec![$(ExtraName::new($x.into()).unwrap()),+]
99+
)
100+
}
101+
102+
#[test]
103+
fn test_no_extra_full() {
104+
let pkg_extras = extras!["dev", "docs", "extra-1", "extra-2"];
105+
let no_extra = extras!["dev", "docs", "extra-1", "extra-2"];
106+
let spec = ExtrasSpecification::from_args(true, no_extra, vec![]);
107+
let result: Vec<_> = spec.extra_names(pkg_extras.iter()).cloned().collect();
108+
assert_eq!(result, extras![]);
109+
}
110+
111+
#[test]
112+
fn test_no_extra_partial() {
113+
let pkg_extras = extras!["dev", "docs", "extra-1", "extra-2"];
114+
let no_extra = extras!["extra-1", "extra-2"];
115+
let spec = ExtrasSpecification::from_args(true, no_extra, vec![]);
116+
let result: Vec<_> = spec.extra_names(pkg_extras.iter()).cloned().collect();
117+
assert_eq!(result, extras!["dev", "docs"]);
118+
}
119+
120+
#[test]
121+
fn test_no_extra_empty() {
122+
let pkg_extras = extras!["dev", "docs", "extra-1", "extra-2"];
123+
let no_extra = extras![];
124+
let spec = ExtrasSpecification::from_args(true, no_extra, vec![]);
125+
let result: Vec<_> = spec.extra_names(pkg_extras.iter()).cloned().collect();
126+
assert_eq!(result, extras!["dev", "docs", "extra-1", "extra-2"]);
127+
}
128+
129+
#[test]
130+
fn test_no_extra_excessive() {
131+
let pkg_extras = extras!["dev", "docs", "extra-1", "extra-2"];
132+
let no_extra = extras!["does-not-exists"];
133+
let spec = ExtrasSpecification::from_args(true, no_extra, vec![]);
134+
let result: Vec<_> = spec.extra_names(pkg_extras.iter()).cloned().collect();
135+
assert_eq!(result, extras!["dev", "docs", "extra-1", "extra-2"]);
136+
}
137+
138+
#[test]
139+
fn test_no_extra_without_all_extras() {
140+
let pkg_extras = extras!["dev", "docs", "extra-1", "extra-2"];
141+
let no_extra = extras!["extra-1", "extra-2"];
142+
let spec = ExtrasSpecification::from_args(false, no_extra, vec![]);
143+
let result: Vec<_> = spec.extra_names(pkg_extras.iter()).cloned().collect();
144+
assert_eq!(result, extras![]);
145+
}
146+
147+
#[test]
148+
fn test_no_extra_without_package_extras() {
149+
let pkg_extras = extras![];
150+
let no_extra = extras!["extra-1", "extra-2"];
151+
let spec = ExtrasSpecification::from_args(true, no_extra, vec![]);
152+
let result: Vec<_> = spec.extra_names(pkg_extras.iter()).cloned().collect();
153+
assert_eq!(result, extras![]);
154+
}
155+
156+
#[test]
157+
fn test_no_extra_duplicates() {
158+
let pkg_extras = extras!["dev", "docs", "extra-1", "extra-1", "extra-2"];
159+
let no_extra = extras!["extra-1", "extra-2"];
160+
let spec = ExtrasSpecification::from_args(true, no_extra, vec![]);
161+
let result: Vec<_> = spec.extra_names(pkg_extras.iter()).cloned().collect();
162+
assert_eq!(result, extras!["dev", "docs"]);
163+
}
164+
165+
#[test]
166+
fn test_no_extra_extra() {
167+
let pkg_extras = extras!["dev", "docs", "extra-1", "extra-2"];
168+
let no_extra = extras!["extra-1", "extra-2"];
169+
let extra = extras!["extra-1", "extra-2", "docs"];
170+
let spec = ExtrasSpecification::from_args(false, no_extra, extra);
171+
let result: Vec<_> = spec.extra_names(pkg_extras.iter()).cloned().collect();
172+
assert_eq!(result, extras!["docs"]);
173+
}
35174
}

crates/uv-requirements/src/source_tree.rs

+6-6
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,11 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
8989
let origin = RequirementOrigin::Project(path.to_path_buf(), metadata.name.clone());
9090

9191
// Determine the extras to include when resolving the requirements.
92-
let extras = match self.extras {
93-
ExtrasSpecification::All => metadata.provides_extras.as_slice(),
94-
ExtrasSpecification::None => &[],
95-
ExtrasSpecification::Some(extras) => extras,
96-
};
92+
let extras: Vec<_> = self
93+
.extras
94+
.extra_names(metadata.provides_extras.iter())
95+
.cloned()
96+
.collect();
9797

9898
// Determine the appropriate requirements to return based on the extras. This involves
9999
// evaluating the `extras` expression in any markers, but preserving the remaining marker
@@ -103,7 +103,7 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
103103
.into_iter()
104104
.map(|requirement| Requirement {
105105
origin: Some(origin.clone()),
106-
marker: requirement.marker.simplify_extras(extras),
106+
marker: requirement.marker.simplify_extras(&extras),
107107
..requirement
108108
})
109109
.collect();

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

+2-12
Original file line numberDiff line numberDiff line change
@@ -77,18 +77,8 @@ impl<'lock> RequirementsTxtExport<'lock> {
7777

7878
// Push its dependencies on the queue.
7979
queue.push_back((dist, None));
80-
match extras {
81-
ExtrasSpecification::None => {}
82-
ExtrasSpecification::All => {
83-
for extra in dist.optional_dependencies.keys() {
84-
queue.push_back((dist, Some(extra)));
85-
}
86-
}
87-
ExtrasSpecification::Some(extras) => {
88-
for extra in extras {
89-
queue.push_back((dist, Some(extra)));
90-
}
91-
}
80+
for extra in extras.extra_names(dist.optional_dependencies.keys()) {
81+
queue.push_back((dist, Some(extra)));
9282
}
9383
}
9484

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

+2-12
Original file line numberDiff line numberDiff line change
@@ -193,18 +193,8 @@ impl<'env> InstallTarget<'env> {
193193
if dev.prod() {
194194
// Push its dependencies on the queue.
195195
queue.push_back((dist, None));
196-
match extras {
197-
ExtrasSpecification::None => {}
198-
ExtrasSpecification::All => {
199-
for extra in dist.optional_dependencies.keys() {
200-
queue.push_back((dist, Some(extra)));
201-
}
202-
}
203-
ExtrasSpecification::Some(extras) => {
204-
for extra in extras {
205-
queue.push_back((dist, Some(extra)));
206-
}
207-
}
196+
for extra in extras.extra_names(dist.optional_dependencies.keys()) {
197+
queue.push_back((dist, Some(extra)));
208198
}
209199
}
210200

crates/uv-settings/src/settings.rs

+10
Original file line numberDiff line numberDiff line change
@@ -1009,6 +1009,16 @@ pub struct PipOptions {
10091009
"#
10101010
)]
10111011
pub all_extras: Option<bool>,
1012+
/// Exclude the specified optional dependencies if `all-extras` is supplied.
1013+
#[option(
1014+
default = "[]",
1015+
value_type = "list[str]",
1016+
example = r#"
1017+
all-extras = true
1018+
no-extra = ["dev", "docs"]
1019+
"#
1020+
)]
1021+
pub no_extra: Option<Vec<ExtraName>>,
10121022
/// Ignore package dependencies, instead only add those packages explicitly listed
10131023
/// on the command line to the resulting the requirements file.
10141024
#[option(

crates/uv/src/settings.rs

+8
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ impl RunSettings {
282282
let RunArgs {
283283
extra,
284284
all_extras,
285+
no_extra,
285286
no_all_extras,
286287
dev,
287288
no_dev,
@@ -323,6 +324,7 @@ impl RunSettings {
323324
frozen,
324325
extras: ExtrasSpecification::from_args(
325326
flag(all_extras, no_all_extras).unwrap_or_default(),
327+
no_extra,
326328
extra.unwrap_or_default(),
327329
),
328330
dev: DevGroupsSpecification::from_args(
@@ -884,6 +886,7 @@ impl SyncSettings {
884886
let SyncArgs {
885887
extra,
886888
all_extras,
889+
no_extra,
887890
no_all_extras,
888891
dev,
889892
no_dev,
@@ -922,6 +925,7 @@ impl SyncSettings {
922925
frozen,
923926
extras: ExtrasSpecification::from_args(
924927
flag(all_extras, no_all_extras).unwrap_or_default(),
928+
no_extra,
925929
extra.unwrap_or_default(),
926930
),
927931
dev: DevGroupsSpecification::from_args(
@@ -1306,6 +1310,7 @@ impl ExportSettings {
13061310
prune,
13071311
extra,
13081312
all_extras,
1313+
no_extra,
13091314
no_all_extras,
13101315
dev,
13111316
no_dev,
@@ -1342,6 +1347,7 @@ impl ExportSettings {
13421347
prune,
13431348
extras: ExtrasSpecification::from_args(
13441349
flag(all_extras, no_all_extras).unwrap_or_default(),
1350+
no_extra,
13451351
extra.unwrap_or_default(),
13461352
),
13471353
dev: DevGroupsSpecification::from_args(
@@ -2490,6 +2496,7 @@ impl PipSettings {
24902496
strict,
24912497
extra,
24922498
all_extras,
2499+
no_extra,
24932500
no_deps,
24942501
allow_empty_requirements,
24952502
resolution,
@@ -2601,6 +2608,7 @@ impl PipSettings {
26012608
),
26022609
extras: ExtrasSpecification::from_args(
26032610
args.all_extras.combine(all_extras).unwrap_or_default(),
2611+
args.no_extra.combine(no_extra).unwrap_or_default(),
26042612
args.extra.combine(extra).unwrap_or_default(),
26052613
),
26062614
dependency_mode: if args.no_deps.combine(no_deps).unwrap_or_default() {

0 commit comments

Comments
 (0)