diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index a521026ca..9eb0991d8 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2702,7 +2702,7 @@ pub struct RunArgs { /// Include dependencies from the specified dependency group. /// /// May be provided multiple times. - #[arg(long, conflicts_with("only_group"))] + #[arg(long, conflicts_with_all = ["only_group", "only_dev"])] pub group: Vec, /// Exclude dependencies from the specified dependency group. @@ -2714,7 +2714,7 @@ pub struct RunArgs { /// Exclude dependencies from default groups. /// /// `--group` can be used to include specific groups. - #[arg(long, conflicts_with_all = ["no_group", "only_group"])] + #[arg(long)] pub no_default_groups: bool, /// Only include dependencies from the specified dependency group. @@ -2722,13 +2722,13 @@ pub struct RunArgs { /// The project itself will also be omitted. /// /// May be provided multiple times. - #[arg(long, conflicts_with("group"))] + #[arg(long, conflicts_with_all = ["group", "dev", "all_groups"])] pub only_group: Vec, /// Include dependencies from all dependency groups. /// /// `--no-group` can be used to exclude specific groups. - #[arg(long, conflicts_with_all = [ "group", "only_group" ])] + #[arg(long, conflicts_with_all = ["only_group", "only_dev"])] pub all_groups: bool, /// Run a Python module. @@ -2742,7 +2742,7 @@ pub struct RunArgs { /// Omit other dependencies. The project itself will also be omitted. /// /// This option is an alias for `--only-group dev`. - #[arg(long, conflicts_with("no_dev"))] + #[arg(long, conflicts_with_all = ["group", "all_groups", "no_dev"])] pub only_dev: bool, /// Install any editable dependencies, including the project and any workspace members, as @@ -2974,7 +2974,7 @@ pub struct SyncArgs { /// Omit other dependencies. The project itself will also be omitted. /// /// This option is an alias for `--only-group dev`. - #[arg(long, conflicts_with("no_dev"))] + #[arg(long, conflicts_with_all = ["group", "all_groups", "no_dev"])] pub only_dev: bool, /// Include dependencies from the specified dependency group. @@ -2983,7 +2983,7 @@ pub struct SyncArgs { /// `tool.uv.conflicts`, uv will report an error. /// /// May be provided multiple times. - #[arg(long, conflicts_with("only_group"))] + #[arg(long, conflicts_with_all = ["only_group", "only_dev"])] pub group: Vec, /// Exclude dependencies from the specified dependency group. @@ -2995,7 +2995,7 @@ pub struct SyncArgs { /// Exclude dependencies from default groups. /// /// `--group` can be used to include specific groups. - #[arg(long, conflicts_with_all = ["no_group", "only_group"])] + #[arg(long)] pub no_default_groups: bool, /// Only include dependencies from the specified dependency group. @@ -3003,13 +3003,13 @@ pub struct SyncArgs { /// The project itself will also be omitted. /// /// May be provided multiple times. - #[arg(long, conflicts_with("group"))] + #[arg(long, conflicts_with_all = ["group", "dev", "all_groups"])] pub only_group: Vec, /// Include dependencies from all dependency groups. /// /// `--no-group` can be used to exclude specific groups. - #[arg(long, conflicts_with_all = [ "group", "only_group" ])] + #[arg(long, conflicts_with_all = ["only_group", "only_dev"])] pub all_groups: bool, /// Install any editable dependencies, including the project and any workspace members, as @@ -3452,7 +3452,7 @@ pub struct TreeArgs { /// Omit other dependencies. The project itself will also be omitted. /// /// This option is an alias for `--only-group dev`. - #[arg(long, conflicts_with("no_dev"))] + #[arg(long, conflicts_with_all = ["group", "all_groups", "no_dev"])] pub only_dev: bool, /// Omit the development dependency group. @@ -3464,7 +3464,7 @@ pub struct TreeArgs { /// Include dependencies from the specified dependency group. /// /// May be provided multiple times. - #[arg(long, conflicts_with("only_group"))] + #[arg(long, conflicts_with_all = ["only_group", "only_dev"])] pub group: Vec, /// Exclude dependencies from the specified dependency group. @@ -3476,7 +3476,7 @@ pub struct TreeArgs { /// Exclude dependencies from default groups. /// /// `--group` can be used to include specific groups. - #[arg(long, conflicts_with_all = ["no_group", "only_group"])] + #[arg(long)] pub no_default_groups: bool, /// Only include dependencies from the specified dependency group. @@ -3484,13 +3484,13 @@ pub struct TreeArgs { /// The project itself will also be omitted. /// /// May be provided multiple times. - #[arg(long, conflicts_with("group"))] + #[arg(long, conflicts_with_all = ["group", "dev", "all_groups"])] pub only_group: Vec, /// Include dependencies from all dependency groups. /// /// `--no-group` can be used to exclude specific groups. - #[arg(long, conflicts_with_all = [ "group", "only_group" ])] + #[arg(long, conflicts_with_all = ["only_group", "only_dev"])] pub all_groups: bool, /// Assert that the `uv.lock` will remain unchanged. @@ -3626,13 +3626,13 @@ pub struct ExportArgs { /// Omit other dependencies. The project itself will also be omitted. /// /// This option is an alias for `--only-group dev`. - #[arg(long, conflicts_with("no_dev"))] + #[arg(long, conflicts_with_all = ["group", "all_groups", "no_dev"])] pub only_dev: bool, /// Include dependencies from the specified dependency group. /// /// May be provided multiple times. - #[arg(long, conflicts_with("only_group"))] + #[arg(long, conflicts_with_all = ["only_group", "only_dev"])] pub group: Vec, /// Exclude dependencies from the specified dependency group. @@ -3644,7 +3644,7 @@ pub struct ExportArgs { /// Exclude dependencies from default groups. /// /// `--group` can be used to include specific groups. - #[arg(long, conflicts_with_all = ["no_group", "only_group"])] + #[arg(long)] pub no_default_groups: bool, /// Only include dependencies from the specified dependency group. @@ -3652,13 +3652,13 @@ pub struct ExportArgs { /// The project itself will also be omitted. /// /// May be provided multiple times. - #[arg(long, conflicts_with("group"))] + #[arg(long, conflicts_with_all = ["group", "dev", "all_groups"])] pub only_group: Vec, /// Include dependencies from all dependency groups. /// /// `--no-group` can be used to exclude specific groups. - #[arg(long, conflicts_with_all = [ "group", "only_group" ])] + #[arg(long, conflicts_with_all = ["only_group", "only_dev"])] pub all_groups: bool, /// Exclude the comment header at the top of the generated output file. diff --git a/crates/uv-configuration/src/dev.rs b/crates/uv-configuration/src/dev.rs index 606f9c63b..7d0620d7c 100644 --- a/crates/uv-configuration/src/dev.rs +++ b/crates/uv-configuration/src/dev.rs @@ -1,9 +1,304 @@ -use std::borrow::Cow; - -use either::Either; +use std::{borrow::Cow, sync::Arc}; use uv_normalize::{GroupName, DEV_DEPENDENCIES}; +/// Manager of all dependency-group decisions and settings history. +/// +/// This is an Arc mostly just to avoid size bloat on things that contain these. +#[derive(Debug, Default, Clone)] +pub struct DevGroupsSpecification(Arc); + +/// Manager of all dependency-group decisions and settings history. +#[derive(Debug, Default, Clone)] +pub struct DevGroupsSpecificationInner { + /// Groups to include. + include: IncludeGroups, + /// Groups to exclude (always wins over include). + exclude: Vec, + /// Whether an `--only` flag was passed. + /// + /// If true, users of this API should refrain from looking at packages + /// that *aren't* specified by the dependency-groups. This is exposed + /// via [`DevGroupsSpecificationInner::prod`][]. + only_groups: bool, + /// The "raw" flags/settings we were passed for diagnostics. + history: DevGroupsSpecificationHistory, +} + +impl DevGroupsSpecification { + /// Create from history. + /// + /// This is the "real" constructor, it's basically taking raw CLI flags but in + /// a way that's a bit nicer for other constructors to use. + fn from_history(history: DevGroupsSpecificationHistory) -> Self { + let DevGroupsSpecificationHistory { + dev_mode, + mut group, + mut only_group, + mut no_group, + all_groups, + no_default_groups, + mut defaults, + } = history.clone(); + + // First desugar --dev flags + match dev_mode { + Some(DevMode::Include) => group.push(DEV_DEPENDENCIES.clone()), + Some(DevMode::Only) => only_group.push(DEV_DEPENDENCIES.clone()), + Some(DevMode::Exclude) => no_group.push(DEV_DEPENDENCIES.clone()), + None => {} + } + + // `group` and `only_group` actually have the same meanings: packages to include. + // But if `only_group` is non-empty then *other* packages should be excluded. + // So we just record whether it was and then treat the two lists as equivalent. + let only_groups = !only_group.is_empty(); + // --only flags imply --no-default-groups + let default_groups = !no_default_groups && !only_groups; + + let include = if all_groups { + // If this is set we can ignore group/only_group/defaults as irrelevant + // (`--all-groups --only-*` is rejected at the CLI level, don't worry about it). + IncludeGroups::All + } else { + // Merge all these lists, they're equivalent now + group.append(&mut only_group); + if default_groups { + group.append(&mut defaults); + } + IncludeGroups::Some(group) + }; + + Self(Arc::new(DevGroupsSpecificationInner { + include, + exclude: no_group, + only_groups, + history, + })) + } + + /// Create from raw CLI args + #[allow(clippy::fn_params_excessive_bools)] + pub fn from_args( + dev: bool, + no_dev: bool, + only_dev: bool, + group: Vec, + no_group: Vec, + no_default_groups: bool, + only_group: Vec, + all_groups: bool, + ) -> Self { + // Lower the --dev flags into a single dev mode. + // + // In theory only one of these 3 flags should be set (enforced by CLI), + // but we explicitly allow `--dev` and `--only-dev` to both be set, + // and "saturate" that to `--only-dev`. + let dev_mode = if only_dev { + Some(DevMode::Only) + } else if no_dev { + Some(DevMode::Exclude) + } else if dev { + Some(DevMode::Include) + } else { + None + }; + + Self::from_history(DevGroupsSpecificationHistory { + dev_mode, + group, + only_group, + no_group, + all_groups, + no_default_groups, + // This is unknown at CLI-time, use `.with_defaults(...)` to apply this later! + defaults: Vec::new(), + }) + } + + /// Helper to make a spec from just a --dev flag + pub fn from_dev_mode(dev_mode: DevMode) -> Self { + Self::from_history(DevGroupsSpecificationHistory { + dev_mode: Some(dev_mode), + ..Default::default() + }) + } + + /// Helper to make a spec from just a --group + pub fn from_group(group: GroupName) -> Self { + Self::from_history(DevGroupsSpecificationHistory { + group: vec![group], + ..Default::default() + }) + } + + /// Apply defaults to a base [`DevGroupsSpecification`]. + /// + /// This is appropriate in projects, where the `dev` group is synced by default. + pub fn with_defaults(&self, defaults: Vec) -> DevGroupsManifest { + // Explicitly clone the inner history and set the defaults, then remake the result. + let mut history = self.0.history.clone(); + history.defaults = defaults; + + DevGroupsManifest { + cur: Self::from_history(history), + prev: self.clone(), + } + } +} + +impl std::ops::Deref for DevGroupsSpecification { + type Target = DevGroupsSpecificationInner; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DevGroupsSpecificationInner { + /// Returns `true` if packages other than the ones referenced by these + /// dependency-groups should be considered. + /// + /// That is, if I tell you to install a project and this is false, + /// you should ignore the project itself and all its dependencies, + /// and instead just install the dependency-groups. + /// + /// (This is really just asking if an --only flag was passed.) + pub fn prod(&self) -> bool { + !self.only_groups + } + + /// Returns `true` if the specification includes the given group. + pub fn contains(&self, group: &GroupName) -> bool { + // exclude always trumps include + !self.exclude.contains(group) && self.include.contains(group) + } + + /// Iterate over all groups that we think should exist. + pub fn desugarred_names(&self) -> impl Iterator { + self.include.names().chain(&self.exclude) + } + + /// Iterate over all groups the user explicitly asked for on the CLI + pub fn explicit_names(&self) -> impl Iterator { + let DevGroupsSpecificationHistory { + // Strictly speaking this is an explicit reference to "dev" + // but we're currently tolerant of dev not existing when referenced with + // these flags, since it kinda implicitly always exists even if + // it's not properly defined in a config file. + dev_mode: _, + group, + only_group, + no_group, + // These reference no groups explicitly + all_groups: _, + no_default_groups: _, + // This doesn't include defaults because the `dev` group may not be defined + // but gets implicitly added as a default sometimes! + defaults: _, + } = self.history(); + + group.iter().chain(no_group).chain(only_group) + } + + /// Returns `true` if the specification will have no effect. + pub fn is_empty(&self) -> bool { + self.prod() && self.exclude.is_empty() && self.include.is_empty() + } + + /// Get the raw history for diagnostics + pub fn history(&self) -> &DevGroupsSpecificationHistory { + &self.history + } +} + +/// Context about a [`DevGroupsSpecification`][] that we've preserved for diagnostics +#[derive(Debug, Default, Clone)] +pub struct DevGroupsSpecificationHistory { + pub dev_mode: Option, + pub group: Vec, + pub only_group: Vec, + pub no_group: Vec, + pub all_groups: bool, + pub no_default_groups: bool, + pub defaults: Vec, +} + +impl DevGroupsSpecificationHistory { + /// Returns all the CLI flags that this represents. + /// + /// If a flag was provided multiple times (e.g. `--group A --group B`) this will + /// elide the arguments and just show the flag once (e.g. just yield "--group"). + /// + /// Conceptually this being an empty list should be equivalent to + /// [`DevGroupsSpecification::is_empty`][] when there aren't any defaults set. + /// When there are defaults the two will disagree, and rightfully so! + pub fn as_flags_pretty(&self) -> Vec> { + let DevGroupsSpecificationHistory { + dev_mode, + group, + only_group, + no_group, + all_groups, + no_default_groups, + // defaults aren't CLI flags! + defaults: _, + } = self; + + let mut flags = vec![]; + if *all_groups { + flags.push(Cow::Borrowed("--all-groups")); + } + if *no_default_groups { + flags.push(Cow::Borrowed("--no-default-groups")); + } + if let Some(dev_mode) = dev_mode { + flags.push(Cow::Borrowed(dev_mode.as_flag())); + } + match &**group { + [] => {} + [group] => flags.push(Cow::Owned(format!("--group {group}"))), + [..] => flags.push(Cow::Borrowed("--group")), + } + match &**only_group { + [] => {} + [group] => flags.push(Cow::Owned(format!("--only-group {group}"))), + [..] => flags.push(Cow::Borrowed("--only-group")), + } + match &**no_group { + [] => {} + [group] => flags.push(Cow::Owned(format!("--no-group {group}"))), + [..] => flags.push(Cow::Borrowed("--no-group")), + } + flags + } +} + +/// A trivial newtype wrapped around [`DevGroupsSpecification`][] that signifies "defaults applied" +/// +/// It includes a copy of the previous semantics to provide info on if +/// the group being a default actually affected it being enabled, because it's obviously "correct". +/// (These are Arcs so it's ~free to hold onto the previous semantics) +#[derive(Debug, Clone)] +pub struct DevGroupsManifest { + /// The active semantics + cur: DevGroupsSpecification, + /// The semantics before defaults were applied + prev: DevGroupsSpecification, +} + +impl DevGroupsManifest { + /// Returns `true` if the specification was enabled, and *only* because it was a default + pub fn contains_because_default(&self, group: &GroupName) -> bool { + self.cur.contains(group) && !self.prev.contains(group) + } +} +impl std::ops::Deref for DevGroupsManifest { + type Target = DevGroupsSpecification; + fn deref(&self) -> &Self::Target { + &self.cur + } +} + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum DevMode { /// Include development dependencies. @@ -16,16 +311,6 @@ pub enum DevMode { } impl DevMode { - /// Returns `true` if the specification allows for production dependencies. - pub fn prod(&self) -> bool { - matches!(self, Self::Exclude | Self::Include) - } - - /// Returns `true` if the specification only includes development dependencies. - pub fn only(&self) -> bool { - matches!(self, Self::Only) - } - /// Returns the flag that was used to request development dependencies. pub fn as_flag(&self) -> &'static str { match self { @@ -34,141 +319,6 @@ impl DevMode { Self::Only => "--only-dev", } } - - /// Returns `true` if the group is `dev`, and development dependencies should be included. - pub fn contains(&self, group: &GroupName) -> bool { - match self { - DevMode::Exclude => false, - DevMode::Include | DevMode::Only => group == &*DEV_DEPENDENCIES, - } - } -} - -#[derive(Default, Debug, Clone)] -pub struct DevGroupsSpecification { - /// Legacy option for `dependency-groups.dev` and `tool.uv.dev-dependencies`. - /// - /// Requested via the `--dev`, `--no-dev`, and `--only-dev` flags. - dev: Option, - - /// The groups to include. - /// - /// Requested via the `--group` and `--only-group` options. - groups: Option, -} - -#[derive(Debug, Clone)] -pub enum GroupsSpecification { - /// 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). - Include { - 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 - /// empty intersection). - Only { - include: Vec, - exclude: Vec, - }, -} - -impl GroupsSpecification { - /// Create a [`GroupsSpecification`] that includes the given group. - pub fn from_group(group: GroupName) -> Self { - Self::Include { - include: IncludeGroups::Some(vec![group]), - exclude: Vec::new(), - } - } - - /// Returns `true` if the specification allows for production dependencies. - pub fn prod(&self) -> bool { - matches!(self, Self::Include { .. } | Self::Explicit { .. }) - } - - /// Returns `true` if the specification is limited to a select set of groups. - pub fn only(&self) -> bool { - matches!(self, Self::Only { .. }) - } - - /// Returns the option that was used to request the groups, if any. - pub fn as_flag(&self) -> Option> { - match self { - Self::Include { include, exclude } => match include { - IncludeGroups::All => Some(Cow::Borrowed("--all-groups")), - IncludeGroups::Some(groups) => match groups.as_slice() { - [] => match exclude.as_slice() { - [] => None, - [group] => Some(Cow::Owned(format!("--no-group {group}"))), - [..] => Some(Cow::Borrowed("--no-group")), - }, - [group] => Some(Cow::Owned(format!("--group {group}"))), - [..] => Some(Cow::Borrowed("--group")), - }, - }, - Self::Only { include, exclude } => match include.as_slice() { - [] => match exclude.as_slice() { - [] => None, - [group] => Some(Cow::Owned(format!("--no-group {group}"))), - [..] => Some(Cow::Borrowed("--no-group")), - }, - [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")), - }, - } - } - - /// Iterate over all groups referenced in the [`GroupsSpecification`]. - pub fn names(&self) -> impl Iterator { - match self { - GroupsSpecification::Include { include, exclude } => { - Either::Left(include.names().chain(exclude.iter())) - } - GroupsSpecification::Only { include, exclude } => { - Either::Left(include.iter().chain(exclude.iter())) - } - GroupsSpecification::Explicit { include } => Either::Right(include.iter()), - } - } - - /// Returns `true` if the specification includes the given group. - pub fn contains(&self, group: &GroupName) -> bool { - match self { - GroupsSpecification::Include { include, exclude } => { - // For `--all-groups`, the group is included unless it is explicitly excluded. - include.contains(group) && !exclude.contains(group) - } - GroupsSpecification::Only { include, .. } => include.contains(group), - GroupsSpecification::Explicit { include } => include.contains(group), - } - } - - /// Returns `true` if the specification will have no effect. - pub fn is_empty(&self) -> bool { - let GroupsSpecification::Include { - include: IncludeGroups::Some(includes), - exclude, - } = self - else { - return false; - }; - includes.is_empty() && exclude.is_empty() - } } #[derive(Debug, Clone)] @@ -188,6 +338,16 @@ impl IncludeGroups { } } + /// Returns `true` if the specification will have no effect. + pub fn is_empty(&self) -> bool { + match self { + IncludeGroups::Some(groups) => groups.is_empty(), + // Although technically this is a noop if they have no groups, + // conceptually they're *trying* to have an effect, so treat it as one. + IncludeGroups::All => false, + } + } + /// Iterate over all groups referenced in the [`IncludeGroups`]. pub fn names(&self) -> std::slice::Iter { match self { @@ -197,249 +357,8 @@ impl IncludeGroups { } } -impl DevGroupsSpecification { - /// Determine the [`DevGroupsSpecification`] policy from the command-line arguments. - #[allow(clippy::fn_params_excessive_bools)] - pub fn from_args( - dev: bool, - no_dev: bool, - only_dev: bool, - mut group: Vec, - no_group: Vec, - no_default_groups: bool, - mut only_group: Vec, - all_groups: bool, - ) -> Self { - let dev = if only_dev { - Some(DevMode::Only) - } else if no_dev { - Some(DevMode::Exclude) - } else if dev { - Some(DevMode::Include) - } else { - None - }; - - 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, - }) - } else if !group.is_empty() { - if matches!(dev, Some(DevMode::Only)) { - unreachable!("cannot specify both `--only-dev` and `--group`") - }; - - // Ensure that `--no-group` and `--group` are mutually exclusive. - group.retain(|group| !no_group.contains(group)); - - Some(GroupsSpecification::Include { - include: IncludeGroups::Some(group), - exclude: no_group, - }) - } else if !only_group.is_empty() { - if matches!(dev, Some(DevMode::Include)) { - unreachable!("cannot specify both `--dev` and `--only-group`") - }; - - // Ensure that `--no-group` and `--only-group` are mutually exclusive. - only_group.retain(|group| !no_group.contains(group)); - - Some(GroupsSpecification::Only { - include: only_group, - exclude: no_group, - }) - } else if !no_group.is_empty() { - Some(GroupsSpecification::Include { - include: IncludeGroups::Some(Vec::new()), - exclude: no_group, - }) - } else { - None - }; - - Self { dev, groups } - } - - /// Return a new [`DevGroupsSpecification`] with development dependencies included by default. - /// - /// This is appropriate in projects, where the `dev` group is synced by default. - #[must_use] - pub fn with_defaults(self, defaults: Vec) -> DevGroupsManifest { - DevGroupsManifest { - spec: self, - defaults, - } - } - - /// Returns `true` if the specification allows for production dependencies. - pub fn prod(&self) -> bool { - self.dev.as_ref().map_or(true, DevMode::prod) - && self.groups.as_ref().map_or(true, GroupsSpecification::prod) - } - - /// Returns `true` if the specification is limited to a select set of groups. - pub fn only(&self) -> bool { - self.dev.as_ref().is_some_and(DevMode::only) - || self.groups.as_ref().is_some_and(GroupsSpecification::only) - } - - /// Returns the flag that was used to request development dependencies, if specified. - pub fn dev_mode(&self) -> Option<&DevMode> { - self.dev.as_ref() - } - - /// Returns the list of groups to include, if specified. - pub fn groups(&self) -> Option<&GroupsSpecification> { - self.groups.as_ref() - } - - /// Returns `true` if the group is included in the specification. - pub fn contains(&self, group: &GroupName) -> bool { - if group == &*DEV_DEPENDENCIES { - match self.dev.as_ref() { - None => {} - Some(DevMode::Exclude) => { - // If `--no-dev` was provided, always exclude dev. - return false; - } - Some(DevMode::Only) => { - // If `--only-dev` was provided, always include dev. - return true; - } - Some(DevMode::Include) => { - // If `--no-group dev` was provided, exclude dev. - return match self.groups.as_ref() { - Some(GroupsSpecification::Include { exclude, .. }) => { - !exclude.contains(group) - } - _ => true, - }; - } - } - } - - self.groups - .as_ref() - .is_some_and(|groups| groups.contains(group)) - } - - /// Returns `true` if the specification will have no effect. - pub fn is_empty(&self) -> bool { - let groups_empty = self - .groups - .as_ref() - .map(GroupsSpecification::is_empty) - .unwrap_or(true); - let dev_empty = self.dev_mode().is_none(); - groups_empty && dev_empty - } -} - -impl From for DevGroupsSpecification { - fn from(dev: DevMode) -> Self { - Self { - dev: Some(dev), - groups: None, - } - } -} - -impl From for DevGroupsSpecification { - fn from(groups: GroupsSpecification) -> Self { - Self { - dev: None, - groups: Some(groups), - } - } -} - -/// The manifest of `dependency-groups` to include, taking into account the user-provided -/// [`DevGroupsSpecification`] and the project-specific default groups. -#[derive(Debug, Default, Clone)] -pub struct DevGroupsManifest { - /// The specification for the development dependencies. - pub(crate) spec: DevGroupsSpecification, - /// The default groups to include. - pub(crate) defaults: Vec, -} - -impl DevGroupsManifest { - /// Returns a new [`DevGroupsManifest`] with the given default groups. - pub fn from_defaults(defaults: Vec) -> Self { - Self { - spec: DevGroupsSpecification::default(), - defaults, - } - } - - /// Returns a new [`DevGroupsManifest`] with the given specification. - pub fn from_spec(spec: DevGroupsSpecification) -> Self { - Self { - spec, - defaults: Vec::new(), - } - } - - /// Returns `true` if the specification allows for production dependencies. - pub fn prod(&self) -> bool { - self.spec.prod() - } - - /// Returns `true` if the group was enabled by default. - pub fn is_default(&self, group: &GroupName) -> bool { - if self.spec.contains(group) { - // If the group was explicitly requested, then it wasn't enabled by default. - false - } else { - // If the group was enabled, but wasn't explicitly requested, then it was enabled by - // default. - self.contains(group) - } - } - - /// Returns `true` if the group is included in the manifest. - pub fn contains(&self, group: &GroupName) -> bool { - if self.spec.contains(group) { - return true; - } - if self.spec.only() { - return false; - } - self.defaults - .iter() - .filter(|default| { - // If `--no-dev` was provided, exclude the `dev` group from the list of defaults. - if matches!(self.spec.dev_mode(), Some(DevMode::Exclude)) { - if *default == &*DEV_DEPENDENCIES { - return false; - }; - } - - // 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: _, - exclude, - }) = self.spec.groups() - { - if exclude.contains(default) { - return false; - } - } - - true - }) - .any(|default| default == group) +impl Default for IncludeGroups { + fn default() -> Self { + Self::Some(Vec::new()) } } diff --git a/crates/uv-requirements/src/source_tree.rs b/crates/uv-requirements/src/source_tree.rs index e50d81be6..94aba87d7 100644 --- a/crates/uv-requirements/src/source_tree.rs +++ b/crates/uv-requirements/src/source_tree.rs @@ -124,14 +124,12 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> { // This is only a warning because *technically* we support passing in // multiple pyproject.tomls, but at this level of abstraction we can't see them all, // so hard erroring on "no pyproject.toml mentions this" is a bit difficult. - if let Some(groups) = self.groups.groups() { - for name in groups.names() { - if !metadata.dependency_groups.contains_key(name) { - warn_user_once!( - "The dependency-group '{name}' is not defined in {}", - path.display() - ); - } + for name in self.groups.explicit_names() { + if !metadata.dependency_groups.contains_key(name) { + warn_user_once!( + "The dependency-group '{name}' is not defined in {}", + path.display() + ); } } diff --git a/crates/uv/src/commands/pip/operations.rs b/crates/uv/src/commands/pip/operations.rs index 48368bd7e..113167063 100644 --- a/crates/uv/src/commands/pip/operations.rs +++ b/crates/uv/src/commands/pip/operations.rs @@ -76,7 +76,11 @@ pub(crate) async fn read_requirements( .into()); } if !groups.is_empty() && !requirements.iter().any(RequirementsSource::allows_groups) { - return Err(anyhow!("Requesting groups requires a `pyproject.toml`.").into()); + let flags = groups.history().as_flags_pretty().join(" "); + return Err(anyhow!( + "Requesting groups requires a `pyproject.toml`. Requested via: {flags}" + ) + .into()); } // Read all requirements from the provided sources. diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 9bae0b526..c5e3429b9 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -16,9 +16,8 @@ use uv_cache::Cache; use uv_cache_key::RepositoryUrl; use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ - Concurrency, Constraints, DevGroupsManifest, DevGroupsSpecification, DevMode, EditableMode, - ExtrasSpecification, GroupsSpecification, InstallOptions, PreviewMode, SourceStrategy, - TrustedHost, + Concurrency, Constraints, DevGroupsSpecification, DevMode, EditableMode, ExtrasSpecification, + InstallOptions, PreviewMode, SourceStrategy, TrustedHost, }; use uv_dispatch::BuildDispatch; use uv_distribution::DistributionDatabase; @@ -831,23 +830,22 @@ async fn lock_and_sync( let (extras, dev) = match dependency_type { DependencyType::Production => { let extras = ExtrasSpecification::None; - let dev = DevGroupsSpecification::from(DevMode::Exclude); + let dev = DevGroupsSpecification::from_dev_mode(DevMode::Exclude); (extras, dev) } DependencyType::Dev => { let extras = ExtrasSpecification::None; - let dev = DevGroupsSpecification::from(DevMode::Include); + let dev = DevGroupsSpecification::from_dev_mode(DevMode::Include); (extras, dev) } DependencyType::Optional(ref extra_name) => { let extras = ExtrasSpecification::Some(vec![extra_name.clone()]); - let dev = DevGroupsSpecification::from(DevMode::Exclude); + let dev = DevGroupsSpecification::from_dev_mode(DevMode::Exclude); (extras, dev) } DependencyType::Group(ref group_name) => { let extras = ExtrasSpecification::None; - let dev = - DevGroupsSpecification::from(GroupsSpecification::from_group(group_name.clone())); + let dev = DevGroupsSpecification::from_group(group_name.clone()); (extras, dev) } }; @@ -869,7 +867,7 @@ async fn lock_and_sync( target, venv, &extras, - &DevGroupsManifest::from_spec(dev), + &dev.with_defaults(Vec::new()), EditableMode::Editable, InstallOptions::default(), Modifications::Sufficient, diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 25eec5ea3..4aa7bbf9b 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -12,7 +12,7 @@ use uv_cache_key::cache_digest; use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ Concurrency, Constraints, DevGroupsManifest, DevGroupsSpecification, ExtrasSpecification, - GroupsSpecification, PreviewMode, Reinstall, TrustedHost, Upgrade, + PreviewMode, Reinstall, TrustedHost, Upgrade, }; use uv_dispatch::{BuildDispatch, SharedState}; use uv_distribution::DistributionDatabase; @@ -288,7 +288,8 @@ impl std::fmt::Display for ConflictError { self.conflicts .iter() .map(|conflict| match conflict { - ConflictPackage::Group(ref group) if self.dev.is_default(group) => + ConflictPackage::Group(ref group) + if self.dev.contains_because_default(group) => format!("`{group}` (enabled by default)"), ConflictPackage::Group(ref group) => format!("`{group}`"), ConflictPackage::Extra(..) => unreachable!(), @@ -307,7 +308,9 @@ impl std::fmt::Display for ConflictError { .map(|(i, conflict)| { let conflict = match conflict { ConflictPackage::Extra(ref extra) => format!("extra `{extra}`"), - ConflictPackage::Group(ref group) if self.dev.is_default(group) => { + ConflictPackage::Group(ref group) + if self.dev.contains_because_default(group) => + { format!("group `{group}` (enabled by default)") } ConflictPackage::Group(ref group) => format!("group `{group}`"), @@ -1841,11 +1844,7 @@ impl DependencyGroupsTarget<'_> { /// Validate the dependency groups requested by the [`DevGroupsSpecification`]. #[allow(clippy::result_large_err)] pub(crate) fn validate(self, dev: &DevGroupsSpecification) -> Result<(), ProjectError> { - for group in dev - .groups() - .into_iter() - .flat_map(GroupsSpecification::names) - { + for group in dev.explicit_names() { match self { Self::Workspace(workspace) => { // The group must be defined in the workspace. diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index c06cab280..eca29a789 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -10,8 +10,8 @@ use tracing::debug; use uv_cache::Cache; use uv_client::Connectivity; use uv_configuration::{ - Concurrency, DevGroupsManifest, EditableMode, ExtrasSpecification, InstallOptions, PreviewMode, - TrustedHost, + Concurrency, DevGroupsSpecification, EditableMode, ExtrasSpecification, InstallOptions, + PreviewMode, TrustedHost, }; use uv_fs::Simplified; use uv_normalize::DEV_DEPENDENCIES; @@ -333,7 +333,7 @@ pub(crate) async fn remove( target, venv, &extras, - &DevGroupsManifest::from_defaults(defaults), + &DevGroupsSpecification::default().with_defaults(defaults), EditableMode::Editable, install_options, Modifications::Exact, diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index f7656b24c..6857f83ce 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -17,8 +17,8 @@ use uv_cache::Cache; use uv_cli::ExternalCommand; use uv_client::{BaseClientBuilder, Connectivity}; use uv_configuration::{ - Concurrency, DevGroupsManifest, DevGroupsSpecification, EditableMode, ExtrasSpecification, - GroupsSpecification, InstallOptions, PreviewMode, SourceStrategy, TrustedHost, + Concurrency, DevGroupsSpecification, EditableMode, ExtrasSpecification, InstallOptions, + PreviewMode, SourceStrategy, TrustedHost, }; use uv_distribution::LoweredRequirement; use uv_fs::which::is_executable; @@ -252,7 +252,7 @@ pub(crate) async fn run( lock: &lock, }, &ExtrasSpecification::default(), - &DevGroupsManifest::default(), + &DevGroupsSpecification::default().with_defaults(Vec::new()), InstallOptions::default(), &settings, &interpreter, @@ -469,14 +469,8 @@ pub(crate) async fn run( if !extras.is_empty() { warn_user!("Extras are not supported for Python scripts with inline metadata"); } - if let Some(dev_mode) = dev.dev_mode() { - warn_user!( - "`{}` is not supported for Python scripts with inline metadata", - dev_mode.as_flag() - ); - } - if let Some(flag) = dev.groups().and_then(GroupsSpecification::as_flag) { - warn_user!("`{flag}` is not supported for Python scripts with inline metadata"); + for flag in dev.history().as_flags_pretty() { + warn_user!("`{flag}` is not supported for Python scripts with inline metadata",); } if all_packages { warn_user!( @@ -544,13 +538,7 @@ pub(crate) async fn run( if !extras.is_empty() { warn_user!("Extras have no effect when used alongside `--no-project`"); } - if let Some(dev_mode) = dev.dev_mode() { - warn_user!( - "`{}` has no effect when used alongside `--no-project`", - dev_mode.as_flag() - ); - } - if let Some(flag) = dev.groups().and_then(GroupsSpecification::as_flag) { + for flag in dev.history().as_flags_pretty() { warn_user!("`{flag}` has no effect when used alongside `--no-project`"); } if locked { @@ -567,13 +555,7 @@ pub(crate) async fn run( if !extras.is_empty() { warn_user!("Extras have no effect when used outside of a project"); } - if let Some(dev_mode) = dev.dev_mode() { - warn_user!( - "`{}` has no effect when used outside of a project", - dev_mode.as_flag() - ); - } - if let Some(flag) = dev.groups().and_then(GroupsSpecification::as_flag) { + for flag in dev.history().as_flags_pretty() { warn_user!("`{flag}` has no effect when used outside of a project"); } if locked { diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 233c1d686..da96add6a 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -364,7 +364,7 @@ impl TestContext { // Exclude `link-mode` on Windows since we set it in the remote test suite if cfg!(windows) { - filters.push(("--link-mode ".to_string(), String::new())); + filters.push((" --link-mode ".to_string(), String::new())); filters.push((r#"link-mode = "copy"\n"#.to_string(), String::new())); } diff --git a/crates/uv/tests/it/run.rs b/crates/uv/tests/it/run.rs index 1a7ebee38..c88b0ada7 100644 --- a/crates/uv/tests/it/run.rs +++ b/crates/uv/tests/it/run.rs @@ -1639,7 +1639,7 @@ fn run_group() -> Result<()> { warning: `--group foo` has no effect when used alongside `--no-project` "###); - uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("--group").arg("bar").arg("--no-project").arg("main.py"), @r###" + uv_snapshot!(context.filters(), context.run().arg("--group").arg("foo").arg("--group").arg("bar").arg("--no-project").arg("main.py"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1649,7 +1649,7 @@ fn run_group() -> Result<()> { ----- stderr ----- warning: `--group` has no effect when used alongside `--no-project` - "###); + "); uv_snapshot!(context.filters(), context.run().arg("--group").arg("dev").arg("--no-project").arg("main.py"), @r###" success: true diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 0dd495bbc..f43a16006 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -144,10 +144,24 @@ fn resolve_uv_toml() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -301,10 +315,24 @@ fn resolve_uv_toml() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -459,10 +487,24 @@ fn resolve_uv_toml() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -649,10 +691,24 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -778,10 +834,24 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -946,10 +1016,24 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -1157,10 +1241,24 @@ fn resolve_index_url() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -1376,10 +1474,24 @@ fn resolve_index_url() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -1558,10 +1670,24 @@ fn resolve_find_links() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -1709,10 +1835,24 @@ fn resolve_top_level() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -1912,10 +2052,24 @@ fn resolve_top_level() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -2098,10 +2252,24 @@ fn resolve_top_level() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -2249,10 +2417,24 @@ fn resolve_user_configuration() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -2383,10 +2565,24 @@ fn resolve_user_configuration() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -2517,10 +2713,24 @@ fn resolve_user_configuration() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -2653,10 +2863,24 @@ fn resolve_user_configuration() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -2967,10 +3191,24 @@ fn resolve_poetry_toml() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -3159,10 +3397,24 @@ fn resolve_both() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -3439,10 +3691,24 @@ fn resolve_config_file() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -3667,10 +3933,24 @@ fn resolve_skip_empty() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -3804,10 +4084,24 @@ fn resolve_skip_empty() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -3960,10 +4254,24 @@ fn allow_insecure_host() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -4169,10 +4477,24 @@ fn index_priority() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -4357,10 +4679,24 @@ fn index_priority() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -4551,10 +4887,24 @@ fn index_priority() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -4740,10 +5090,24 @@ fn index_priority() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -4936,10 +5300,24 @@ fn index_priority() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -5125,10 +5503,24 @@ fn index_priority() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -5267,10 +5659,24 @@ fn verify_hashes() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -5395,10 +5801,24 @@ fn verify_hashes() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -5521,10 +5941,24 @@ fn verify_hashes() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -5649,10 +6083,24 @@ fn verify_hashes() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -5775,10 +6223,24 @@ fn verify_hashes() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, @@ -5902,10 +6364,24 @@ fn verify_hashes() -> anyhow::Result<()> { }, system: false, extras: None, - groups: DevGroupsSpecification { - dev: None, - groups: None, - }, + groups: DevGroupsSpecification( + DevGroupsSpecificationInner { + include: Some( + [], + ), + exclude: [], + only_groups: false, + history: DevGroupsSpecificationHistory { + dev_mode: None, + group: [], + only_group: [], + no_group: [], + all_groups: false, + no_default_groups: false, + defaults: [], + }, + }, + ), break_system_packages: false, target: None, prefix: None, diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 44e99803b..06cf83363 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -1782,6 +1782,223 @@ fn sync_non_existent_group() -> Result<()> { Ok(()) } +#[test] +fn sync_corner_groups() -> Result<()> { + // Testing a bunch of random corner cases of flags so their behaviour is tracked. + // It's fine if we decide we want to support these later! + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [dependency-groups] + dev = ["iniconfig"] + foo = ["sniffio"] + bar = ["requests"] + "#, + )?; + + context.lock().assert().success(); + + // --no-dev and --only-dev should error + // (This one could be made to work with overloading) + uv_snapshot!(context.filters(), context.sync() + .arg("--no-dev") + .arg("--only-dev"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: the argument '--no-dev' cannot be used with '--only-dev' + + Usage: uv sync --cache-dir [CACHE_DIR] --no-dev --exclude-newer + + For more information, try '--help'. + "); + + // --dev and --only-group should error if they don't match + uv_snapshot!(context.filters(), context.sync() + .arg("--dev") + .arg("--only-group").arg("bar"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: the argument '--dev' cannot be used with '--only-group ' + + Usage: uv sync --cache-dir [CACHE_DIR] --exclude-newer + + For more information, try '--help'. + "); + + // --dev and --only-group should error even if it's dev still + // (This one could be made to work the same as --dev --only-dev) + uv_snapshot!(context.filters(), context.sync() + .arg("--dev") + .arg("--only-group").arg("dev"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: the argument '--dev' cannot be used with '--only-group ' + + Usage: uv sync --cache-dir [CACHE_DIR] --exclude-newer + + For more information, try '--help'. + "); + + // --group and --only-dev should error if they don't match + // (This one could be made to work the same as --dev --only-dev) + uv_snapshot!(context.filters(), context.sync() + .arg("--only-dev") + .arg("--group").arg("bar"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: the argument '--only-dev' cannot be used with '--group ' + + Usage: uv sync --cache-dir [CACHE_DIR] --only-dev --exclude-newer + + For more information, try '--help'. + "); + + // --group and --only-dev should error even if it's dev still + // (This one could be made to work the same as --dev --only-dev) + uv_snapshot!(context.filters(), context.sync() + .arg("--only-dev") + .arg("--group").arg("dev"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: the argument '--only-dev' cannot be used with '--group ' + + Usage: uv sync --cache-dir [CACHE_DIR] --only-dev --exclude-newer + + For more information, try '--help'. + "); + + // --all-groups and --only-dev should error + uv_snapshot!(context.filters(), context.sync() + .arg("--all-groups") + .arg("--only-dev"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: the argument '--all-groups' cannot be used with '--only-dev' + + Usage: uv sync --cache-dir [CACHE_DIR] --all-groups --exclude-newer + + For more information, try '--help'. + "); + + // --all-groups and --only-group should error + uv_snapshot!(context.filters(), context.sync() + .arg("--all-groups") + .arg("--only-group").arg("bar"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: the argument '--all-groups' cannot be used with '--only-group ' + + Usage: uv sync --cache-dir [CACHE_DIR] --all-groups --exclude-newer + + For more information, try '--help'. + "); + + // --group and --only-group should error if they name disjoint things + uv_snapshot!(context.filters(), context.sync() + .arg("--group").arg("foo") + .arg("--only-group").arg("bar"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: the argument '--group ' cannot be used with '--only-group ' + + Usage: uv sync --cache-dir [CACHE_DIR] --group --exclude-newer + + For more information, try '--help'. + "); + + // --group and --only-group should error if they name same things + // (This one would be fair to allow, but... is it worth it?) + uv_snapshot!(context.filters(), context.sync() + .arg("--group").arg("foo") + .arg("--only-group").arg("foo"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: the argument '--group ' cannot be used with '--only-group ' + + Usage: uv sync --cache-dir [CACHE_DIR] --group --exclude-newer + + For more information, try '--help'. + "); + + // --all-groups and --no-default-groups is redundant but should be --all-groups + uv_snapshot!(context.filters(), context.sync() + .arg("--all-groups") + .arg("--no-default-groups"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 9 packages in [TIME] + Prepared 8 packages in [TIME] + Installed 8 packages in [TIME] + + certifi==2024.2.2 + + charset-normalizer==3.3.2 + + idna==3.6 + + iniconfig==2.0.0 + + requests==2.31.0 + + sniffio==1.3.1 + + typing-extensions==4.10.0 + + urllib3==2.2.1 + "); + + // --dev --only-dev should saturate as --only-dev + uv_snapshot!(context.filters(), context.sync() + .arg("--dev") + .arg("--only-dev"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 9 packages in [TIME] + Uninstalled 7 packages in [TIME] + - 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 + "); + Ok(()) +} + #[test] fn sync_non_existent_default_group() -> Result<()> { let context = TestContext::new("3.12");