diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 6ff408ea19b0..2594b047b01e 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2716,6 +2716,12 @@ pub struct RunArgs { #[arg(long)] pub no_group: Vec, + /// Exclude dependencies from default groups. + /// + /// `--group` can be used to include specific groups. + #[arg(long, conflicts_with_all = ["no_group", "only_group"])] + pub no_default_groups: bool, + /// Only include dependencies from the specified dependency group. /// /// May be provided multiple times. @@ -2981,6 +2987,12 @@ pub struct SyncArgs { #[arg(long)] pub no_group: Vec, + /// Exclude dependencies from default groups. + /// + /// `--group` can be used to include specific groups. + #[arg(long, conflicts_with_all = ["no_group", "only_group"])] + pub no_default_groups: bool, + /// Only include dependencies from the specified dependency group. /// /// May be provided multiple times. @@ -3426,6 +3438,12 @@ pub struct TreeArgs { #[arg(long)] pub no_group: Vec, + /// Exclude dependencies from default groups. + /// + /// `--group` can be used to include specific groups. + #[arg(long, conflicts_with_all = ["no_group", "only_group"])] + pub no_default_groups: bool, + /// Only include dependencies from the specified dependency group. /// /// May be provided multiple times. @@ -3590,6 +3608,12 @@ pub struct ExportArgs { #[arg(long)] pub no_group: Vec, + /// Exclude dependencies from default groups. + /// + /// `--group` can be used to include specific groups. + #[arg(long, conflicts_with_all = ["no_group", "only_group"])] + pub no_default_groups: bool, + /// Only include dependencies from the specified dependency group. /// /// May be provided multiple times. diff --git a/crates/uv-configuration/src/dev.rs b/crates/uv-configuration/src/dev.rs index 3a94c6f4be16..8a2a43d2ce11 100644 --- a/crates/uv-configuration/src/dev.rs +++ b/crates/uv-configuration/src/dev.rs @@ -1,5 +1,7 @@ use std::borrow::Cow; +use either::Either; + use uv_normalize::{GroupName, DEV_DEPENDENCIES}; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] @@ -57,7 +59,8 @@ pub struct DevGroupsSpecification { #[derive(Debug, Clone)] pub enum GroupsSpecification { - /// Include dependencies from the specified groups. + /// Include dependencies from the specified groups alongside the default groups (omitting + /// those default groups that are explicitly excluded). /// /// If the `include` is `IncludeGroups::Some`, it is guaranteed to omit groups in the `exclude` /// list (i.e., they have an empty intersection). @@ -65,6 +68,10 @@ pub enum GroupsSpecification { include: IncludeGroups, exclude: Vec, }, + /// Include dependencies from the specified groups, omitting any default groups. + /// + /// If the list is empty, no group will be included. + Explicit { include: Vec }, /// Only include dependencies from the specified groups, exclude all other dependencies. /// /// The `include` list is guaranteed to omit groups in the `exclude` list (i.e., they have an @@ -86,7 +93,7 @@ impl GroupsSpecification { /// Returns `true` if the specification allows for production dependencies. pub fn prod(&self) -> bool { - matches!(self, Self::Include { .. }) + matches!(self, Self::Include { .. } | Self::Explicit { .. }) } /// Returns `true` if the specification is limited to a select set of groups. @@ -118,6 +125,11 @@ impl GroupsSpecification { [group] => Some(Cow::Owned(format!("--only-group {group}"))), [..] => Some(Cow::Borrowed("--only-group")), }, + Self::Explicit { include } => match include.as_slice() { + [] => Some(Cow::Borrowed("--no-default-groups")), + [group] => Some(Cow::Owned(format!("--group {group}"))), + [..] => Some(Cow::Borrowed("--group")), + }, } } @@ -125,9 +137,12 @@ impl GroupsSpecification { pub fn names(&self) -> impl Iterator { match self { GroupsSpecification::Include { include, exclude } => { - include.names().chain(exclude.iter()) + Either::Left(include.names().chain(exclude.iter())) + } + GroupsSpecification::Only { include, exclude } => { + Either::Left(include.iter().chain(exclude.iter())) } - GroupsSpecification::Only { include, exclude } => include.iter().chain(exclude.iter()), + GroupsSpecification::Explicit { include } => Either::Right(include.iter()), } } @@ -139,6 +154,7 @@ impl GroupsSpecification { include.contains(group) && !exclude.contains(group) } GroupsSpecification::Only { include, .. } => include.contains(group), + GroupsSpecification::Explicit { include } => include.contains(group), } } } @@ -178,6 +194,7 @@ impl DevGroupsSpecification { only_dev: bool, mut group: Vec, no_group: Vec, + no_default_groups: bool, mut only_group: Vec, all_groups: bool, ) -> Self { @@ -191,7 +208,12 @@ impl DevGroupsSpecification { None }; - let groups = if all_groups { + let groups = if no_default_groups { + // Remove groups specified with `--no-group`. + group.retain(|group| !no_group.contains(group)); + + Some(GroupsSpecification::Explicit { include: group }) + } else if all_groups { Some(GroupsSpecification::Include { include: IncludeGroups::All, exclude: no_group, @@ -376,6 +398,12 @@ impl DevGroupsManifest { }; } + // If `--no-default-groups` was provided, only include group if it's explicitly + // included with `--group `. + if let Some(GroupsSpecification::Explicit { include }) = self.spec.groups() { + return include.contains(group); + } + // If `--no-group` was provided, exclude the group from the list of defaults. if let Some(GroupsSpecification::Include { include: _, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index c135845d6c7f..2637fff91e8d 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -300,6 +300,7 @@ impl RunSettings { no_dev, group, no_group, + no_default_groups, only_group, all_groups, module: _, @@ -343,7 +344,14 @@ impl RunSettings { extra.unwrap_or_default(), ), dev: DevGroupsSpecification::from_args( - dev, no_dev, only_dev, group, no_group, only_group, all_groups, + dev, + no_dev, + only_dev, + group, + no_group, + no_default_groups, + only_group, + all_groups, ), editable: EditableMode::from_args(no_editable), modifications: if flag(exact, inexact).unwrap_or(false) { @@ -960,6 +968,7 @@ impl SyncSettings { only_dev, group, no_group, + no_default_groups, only_group, all_groups, no_editable, @@ -996,7 +1005,14 @@ impl SyncSettings { extra.unwrap_or_default(), ), dev: DevGroupsSpecification::from_args( - dev, no_dev, only_dev, group, no_group, only_group, all_groups, + dev, + no_dev, + only_dev, + group, + no_group, + no_default_groups, + only_group, + all_groups, ), editable: EditableMode::from_args(no_editable), install_options: InstallOptions::new( @@ -1316,6 +1332,7 @@ impl TreeSettings { no_dev, group, no_group, + no_default_groups, only_group, all_groups, locked, @@ -1334,7 +1351,14 @@ impl TreeSettings { Self { dev: DevGroupsSpecification::from_args( - dev, no_dev, only_dev, group, no_group, only_group, all_groups, + dev, + no_dev, + only_dev, + group, + no_group, + no_default_groups, + only_group, + all_groups, ), locked, frozen, @@ -1397,6 +1421,7 @@ impl ExportSettings { only_dev, group, no_group, + no_default_groups, only_group, all_groups, header, @@ -1432,7 +1457,14 @@ impl ExportSettings { extra.unwrap_or_default(), ), dev: DevGroupsSpecification::from_args( - dev, no_dev, only_dev, group, no_group, only_group, all_groups, + dev, + no_dev, + only_dev, + group, + no_group, + no_default_groups, + only_group, + all_groups, ), editable: EditableMode::from_args(no_editable), hashes: flag(hashes, no_hashes).unwrap_or(true), diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 5e92049f3810..3109d5cc238c 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -1259,6 +1259,20 @@ fn sync_dev() -> Result<()> { + sniffio==1.3.1 "###); + // Using `--no-default-groups` should remove dev dependencies + uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 5 packages in [TIME] + Uninstalled 3 packages in [TIME] + - anyio==4.3.0 + - idna==3.6 + - sniffio==1.3.1 + "###); + Ok(()) } @@ -1427,6 +1441,74 @@ fn sync_group() -> Result<()> { Audited 1 package in [TIME] "###); + uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Installed 8 packages in [TIME] + + anyio==4.3.0 + + certifi==2024.2.2 + + charset-normalizer==3.3.2 + + idna==3.6 + + iniconfig==2.0.0 + + requests==2.31.0 + + sniffio==1.3.1 + + urllib3==2.2.1 + "###); + + // Using `--no-default-groups` should exclude all groups + uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Uninstalled 8 packages in [TIME] + - anyio==4.3.0 + - certifi==2024.2.2 + - charset-normalizer==3.3.2 + - idna==3.6 + - iniconfig==2.0.0 + - requests==2.31.0 + - sniffio==1.3.1 + - urllib3==2.2.1 + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Installed 8 packages in [TIME] + + anyio==4.3.0 + + certifi==2024.2.2 + + charset-normalizer==3.3.2 + + idna==3.6 + + iniconfig==2.0.0 + + requests==2.31.0 + + sniffio==1.3.1 + + urllib3==2.2.1 + "###); + + // Using `--no-default-groups` with `--group foo` and `--group bar` should include those groups, + // excluding the remaining `dev` group. + uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups").arg("--group").arg("foo").arg("--group").arg("bar"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Uninstalled 1 package in [TIME] + - iniconfig==2.0.0 + "###); + Ok(()) } @@ -1528,6 +1610,44 @@ fn sync_include_group() -> Result<()> { + typing-extensions==4.10.0 "###); + uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Uninstalled 4 packages in [TIME] + - anyio==4.3.0 + - idna==3.6 + - iniconfig==2.0.0 + - sniffio==1.3.1 + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + iniconfig==2.0.0 + + sniffio==1.3.1 + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups").arg("--group").arg("foo"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 6 packages in [TIME] + Audited 5 packages in [TIME] + "###); + Ok(()) } @@ -1923,6 +2043,74 @@ fn sync_default_groups() -> Result<()> { - urllib3==2.2.1 "###); + uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Installed 8 packages in [TIME] + + anyio==4.3.0 + + certifi==2024.2.2 + + charset-normalizer==3.3.2 + + idna==3.6 + + requests==2.31.0 + + sniffio==1.3.1 + + typing-extensions==4.10.0 + + urllib3==2.2.1 + "###); + + // Using `--no-default-groups` should exclude all groups + uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Uninstalled 8 packages in [TIME] + - anyio==4.3.0 + - certifi==2024.2.2 + - charset-normalizer==3.3.2 + - idna==3.6 + - iniconfig==2.0.0 + - requests==2.31.0 + - sniffio==1.3.1 + - urllib3==2.2.1 + "###); + + uv_snapshot!(context.filters(), context.sync().arg("--all-groups"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Installed 8 packages in [TIME] + + anyio==4.3.0 + + certifi==2024.2.2 + + charset-normalizer==3.3.2 + + idna==3.6 + + iniconfig==2.0.0 + + requests==2.31.0 + + sniffio==1.3.1 + + urllib3==2.2.1 + "###); + + // Using `--no-default-groups` with `--group foo` and `--group bar` should include those groups, + // excluding the remaining `dev` group. + uv_snapshot!(context.filters(), context.sync().arg("--no-default-groups").arg("--group").arg("foo").arg("--group").arg("bar"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 10 packages in [TIME] + Uninstalled 1 package in [TIME] + - iniconfig==2.0.0 + "###); + Ok(()) } diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 5a81bdcf6b68..aa5439446480 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -320,6 +320,10 @@ uv run [OPTIONS] [COMMAND]

Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.

May also be set with the UV_NO_CONFIG environment variable.

+
--no-default-groups

Exclude dependencies from default groups.

+ +

--group can be used to include specific groups.

+
--no-dev

Omit the development dependency group.

This option is an alias of --no-group dev.

@@ -1685,6 +1689,10 @@ uv sync [OPTIONS]

Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.

May also be set with the UV_NO_CONFIG environment variable.

+
--no-default-groups

Exclude dependencies from default groups.

+ +

--group can be used to include specific groups.

+
--no-dev

Omit the development dependency group.

This option is an alias for --no-group dev.

@@ -2391,6 +2399,10 @@ uv export [OPTIONS]

Normally, configuration files are discovered in the current directory, parent directories, or user configuration directories.

May also be set with the UV_NO_CONFIG environment variable.

+
--no-default-groups

Exclude dependencies from default groups.

+ +

--group can be used to include specific groups.

+
--no-dev

Omit the development dependency group.

This option is an alias for --no-group dev.

@@ -2765,6 +2777,10 @@ uv tree [OPTIONS]

May also be set with the UV_NO_CONFIG environment variable.

--no-dedupe

Do not de-duplicate repeated dependencies. Usually, when a package has already displayed its dependencies, further occurrences will not re-display its dependencies, and will include a (*) to indicate it has already been shown. This flag will cause those duplicates to be repeated

+
--no-default-groups

Exclude dependencies from default groups.

+ +

--group can be used to include specific groups.

+
--no-dev

Omit the development dependency group.

This option is an alias for --no-group dev.