diff --git a/crates/uv-configuration/src/dev.rs b/crates/uv-configuration/src/dev.rs index b0a0cf9029b2..8b14d3be16f5 100644 --- a/crates/uv-configuration/src/dev.rs +++ b/crates/uv-configuration/src/dev.rs @@ -13,31 +13,44 @@ pub enum DevMode { } impl DevMode { - /// Determine the [`DevMode`] policy from the command-line arguments. - pub fn from_args(dev: bool, no_dev: bool, only_dev: bool) -> Self { - if only_dev { - Self::Only - } else if no_dev { - Self::Exclude - } else if dev { - Self::Include - } else { - Self::default() + /// Iterate over the group names to include. + pub fn iter(&self) -> impl Iterator { + match self { + Self::Exclude => Either::Left(std::iter::empty()), + Self::Include | Self::Only => Either::Right(std::iter::once(&*DEV_DEPENDENCIES)), } } + + /// Returns `true` if the specification allows for production dependencies. + pub fn prod(&self) -> bool { + matches!(self, Self::Exclude | Self::Include) + } } #[derive(Debug, Clone)] -pub enum DevSpecification { - /// Include dev dependencies from the specified group. +pub struct DevGroupsSpecification { + /// Legacy option for `dependency-group.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: GroupsSpecification, +} + +#[derive(Debug, Clone)] +pub enum GroupsSpecification { + /// Include dependencies from the specified groups. Include(Vec), - /// Do not include dev dependencies. + /// Do not include dependencies from groups. Exclude, - /// Include dev dependencies from the specified groups, and exclude all non-dev dependencies. + /// Only include dependencies from the specified groups, exclude all other dependencies. Only(Vec), } -impl DevSpecification { +impl GroupsSpecification { /// Returns an [`Iterator`] over the group names to include. pub fn iter(&self) -> impl Iterator { match self { @@ -52,18 +65,16 @@ impl DevSpecification { } } -impl From for DevSpecification { - fn from(mode: DevMode) -> Self { - match mode { - DevMode::Include => Self::Include(vec![DEV_DEPENDENCIES.clone()]), - DevMode::Exclude => Self::Exclude, - DevMode::Only => Self::Only(vec![DEV_DEPENDENCIES.clone()]), +impl DevGroupsSpecification { + /// Returns an [`Iterator`] over the group names to include. + pub fn iter(&self) -> impl Iterator { + match self.dev { + None => Either::Left(self.groups.iter()), + Some(ref dev_mode) => Either::Right(self.groups.iter().chain(dev_mode.iter())), } } -} -impl DevSpecification { - /// Determine the [`DevSpecification`] policy from the command-line arguments. + /// Determine the [`DevGroupsSpecification`] policy from the command-line arguments. pub fn from_args( dev: bool, no_dev: bool, @@ -71,29 +82,75 @@ impl DevSpecification { group: Vec, only_group: Vec, ) -> Self { - let from_mode = DevSpecification::from(DevMode::from_args(dev, no_dev, only_dev)); - if !group.is_empty() { - match from_mode { - DevSpecification::Exclude => Self::Include(group), - DevSpecification::Include(dev) => { - Self::Include(group.into_iter().chain(dev).collect()) - } - DevSpecification::Only(_) => { - unreachable!("cannot specify both `--only-dev` and `--group`") - } - } + let dev_mode = if only_dev { + Some(DevMode::Only) + } else if no_dev { + Some(DevMode::Exclude) + } else if dev { + Some(DevMode::Include) + } else { + None + }; + + let groups = if !group.is_empty() { + if matches!(dev_mode, Some(DevMode::Only)) { + unreachable!("cannot specify both `--only-dev` and `--group`") + }; + GroupsSpecification::Include(group) } else if !only_group.is_empty() { - match from_mode { - DevSpecification::Exclude => Self::Only(only_group), - DevSpecification::Only(dev) => { - Self::Only(only_group.into_iter().chain(dev).collect()) - } - // TODO(zanieb): `dev` defaults to true we can't tell if `--dev` was provided in - // conflict with `--only-group` here - DevSpecification::Include(_) => Self::Only(only_group), - } + if matches!(dev_mode, Some(DevMode::Include)) { + unreachable!("cannot specify both `--dev` and `--only-group`") + }; + GroupsSpecification::Only(only_group) } else { - from_mode + GroupsSpecification::Exclude + }; + + Self { + dev: dev_mode, + 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_default_dev(self) -> Self { + match self.dev { + Some(_) => self, + None => match self.groups { + // Only include the default `dev` group if `--only-group` wasn't used + GroupsSpecification::Only(_) => self, + GroupsSpecification::Exclude | GroupsSpecification::Include(_) => Self { + dev: Some(DevMode::Include), + ..self + }, + }, } } + + /// Returns `true` if the specification allows for production dependencies. + pub fn prod(&self) -> bool { + (self.dev.is_none() || self.dev.as_ref().is_some_and(DevMode::prod)) && self.groups.prod() + } + + pub fn dev_mode(&self) -> Option<&DevMode> { + self.dev.as_ref() + } +} + +impl From for DevGroupsSpecification { + fn from(dev: DevMode) -> Self { + Self { + dev: Some(dev), + groups: GroupsSpecification::Exclude, + } + } +} + +impl From for DevGroupsSpecification { + fn from(groups: GroupsSpecification) -> Self { + Self { dev: None, groups } + } } diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 716372a1d7b7..0f61d39d3a3d 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -15,7 +15,7 @@ use toml_edit::{value, Array, ArrayOfTables, InlineTable, Item, Table, Value}; use url::Url; use uv_cache_key::RepositoryUrl; -use uv_configuration::{BuildOptions, DevSpecification, ExtrasSpecification, InstallOptions}; +use uv_configuration::{BuildOptions, DevGroupsSpecification, ExtrasSpecification, InstallOptions}; use uv_distribution::DistributionDatabase; use uv_distribution_filename::{DistExtension, ExtensionError, SourceDistExtension, WheelFilename}; use uv_distribution_types::{ @@ -575,7 +575,7 @@ impl Lock { marker_env: &ResolverMarkerEnvironment, tags: &Tags, extras: &ExtrasSpecification, - dev: &DevSpecification, + dev: &DevGroupsSpecification, build_options: &BuildOptions, install_options: &InstallOptions, ) -> Result { diff --git a/crates/uv-resolver/src/lock/requirements_txt.rs b/crates/uv-resolver/src/lock/requirements_txt.rs index b6f30385e3c2..9c473a8f3856 100644 --- a/crates/uv-resolver/src/lock/requirements_txt.rs +++ b/crates/uv-resolver/src/lock/requirements_txt.rs @@ -10,7 +10,7 @@ use petgraph::{Directed, Graph}; use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; use url::Url; -use uv_configuration::{DevSpecification, EditableMode, ExtrasSpecification, InstallOptions}; +use uv_configuration::{DevGroupsSpecification, EditableMode, ExtrasSpecification, InstallOptions}; use uv_distribution_filename::{DistExtension, SourceDistExtension}; use uv_fs::Simplified; use uv_git::GitReference; @@ -43,7 +43,7 @@ impl<'lock> RequirementsTxtExport<'lock> { lock: &'lock Lock, root_name: &PackageName, extras: &ExtrasSpecification, - dev: &DevSpecification, + dev: &DevGroupsSpecification, editable: EditableMode, hashes: bool, install_options: &'lock InstallOptions, diff --git a/crates/uv-resolver/src/lock/tree.rs b/crates/uv-resolver/src/lock/tree.rs index 3ce6a2dbf311..0f4bbb70e38f 100644 --- a/crates/uv-resolver/src/lock/tree.rs +++ b/crates/uv-resolver/src/lock/tree.rs @@ -4,7 +4,7 @@ use std::collections::BTreeSet; use itertools::Itertools; use rustc_hash::{FxHashMap, FxHashSet}; -use uv_configuration::DevMode; +use uv_configuration::DevGroupsSpecification; use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_pypi_types::ResolverMarkerEnvironment; @@ -26,7 +26,7 @@ pub struct TreeDisplay<'env> { /// Maximum display depth of the dependency tree. depth: usize, /// Whether to include development dependencies in the display. - dev: DevMode, + dev: DevGroupsSpecification, /// Prune the given packages from the display of the dependency tree. prune: Vec, /// Display only the specified packages. @@ -43,7 +43,7 @@ impl<'env> TreeDisplay<'env> { depth: usize, prune: Vec, packages: Vec, - dev: DevMode, + dev: DevGroupsSpecification, no_dedupe: bool, invert: bool, ) -> Self { @@ -236,14 +236,14 @@ impl<'env> TreeDisplay<'env> { let dependencies: Vec> = self .dependencies .get(node.package_id()) - .filter(|_| self.dev != DevMode::Only) + .filter(|_| self.dev.prod()) .into_iter() .flatten() .map(|dep| Node::Dependency(dep.as_ref())) .chain( self.optional_dependencies .get(node.package_id()) - .filter(|_| self.dev != DevMode::Only) + .filter(|_| self.dev.prod()) .into_iter() .flatten() .flat_map(|(extra, deps)| { @@ -254,9 +254,9 @@ impl<'env> TreeDisplay<'env> { .chain( self.dev_dependencies .get(node.package_id()) - .filter(|_| self.dev != DevMode::Exclude) .into_iter() .flatten() + .filter(|(group, _)| self.dev.iter().contains(*group)) .flat_map(|(group, deps)| { deps.iter().map(move |dep| Node::DevDependency(group, dep)) }), diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 02147a148893..77e808380823 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -13,8 +13,8 @@ use uv_cache::Cache; use uv_cache_key::RepositoryUrl; use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ - Concurrency, Constraints, DevMode, DevSpecification, EditableMode, ExtrasSpecification, - InstallOptions, LowerBound, SourceStrategy, + Concurrency, Constraints, DevGroupsSpecification, DevMode, EditableMode, ExtrasSpecification, + GroupsSpecification, InstallOptions, LowerBound, SourceStrategy, }; use uv_dispatch::BuildDispatch; use uv_distribution::DistributionDatabase; @@ -807,22 +807,25 @@ async fn lock_and_sync( let (extras, dev) = match dependency_type { DependencyType::Production => { let extras = ExtrasSpecification::None; - let dev = DevSpecification::from(DevMode::Exclude); + let dev = DevGroupsSpecification::from(DevMode::Exclude); (extras, dev) } DependencyType::Dev => { let extras = ExtrasSpecification::None; - let dev = DevSpecification::from(DevMode::Include); + let dev = DevGroupsSpecification::from(DevMode::Include); (extras, dev) } DependencyType::Optional(ref extra_name) => { let extras = ExtrasSpecification::Some(vec![extra_name.clone()]); - let dev = DevSpecification::from(DevMode::Exclude); + let dev = DevGroupsSpecification::from(DevMode::Exclude); (extras, dev) } DependencyType::Group(ref group_name) => { let extras = ExtrasSpecification::None; - let dev = DevSpecification::Include(vec![group_name.clone()]); + let dev = + DevGroupsSpecification::from(GroupsSpecification::Include( + vec![group_name.clone()], + )); (extras, dev) } }; diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index 8722cac05804..549a53815cd0 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -8,7 +8,7 @@ use std::path::{Path, PathBuf}; use uv_cache::Cache; use uv_client::Connectivity; use uv_configuration::{ - Concurrency, DevMode, DevSpecification, EditableMode, ExportFormat, ExtrasSpecification, + Concurrency, DevGroupsSpecification, EditableMode, ExportFormat, ExtrasSpecification, InstallOptions, LowerBound, }; use uv_normalize::PackageName; @@ -33,7 +33,7 @@ pub(crate) async fn export( install_options: InstallOptions, output_file: Option, extras: ExtrasSpecification, - dev: DevMode, + dev: DevGroupsSpecification, editable: EditableMode, locked: bool, frozen: bool, @@ -141,7 +141,7 @@ pub(crate) async fn export( &lock, project.project_name(), &extras, - &DevSpecification::from(dev), + &dev.with_default_dev(), editable, hashes, &install_options, diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index c717074f1a13..a689d5daf944 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -6,8 +6,8 @@ use owo_colors::OwoColorize; use uv_cache::Cache; use uv_client::Connectivity; use uv_configuration::{ - Concurrency, DevMode, DevSpecification, EditableMode, ExtrasSpecification, InstallOptions, - LowerBound, + Concurrency, DevGroupsSpecification, DevMode, EditableMode, ExtrasSpecification, + InstallOptions, LowerBound, }; use uv_fs::Simplified; use uv_pep508::PackageName; @@ -215,7 +215,7 @@ pub(crate) async fn remove( &venv, &lock, &extras, - &DevSpecification::from(dev), + &DevGroupsSpecification::from(dev), 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 e69cb2e4cbf1..ada122e8a7b7 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, DevMode, DevSpecification, EditableMode, ExtrasSpecification, InstallOptions, - LowerBound, SourceStrategy, + Concurrency, DevGroupsSpecification, DevMode, EditableMode, ExtrasSpecification, + InstallOptions, LowerBound, SourceStrategy, }; use uv_distribution::LoweredRequirement; use uv_fs::which::is_executable; @@ -68,7 +68,7 @@ pub(crate) async fn run( no_project: bool, no_config: bool, extras: ExtrasSpecification, - dev: DevMode, + dev: DevGroupsSpecification, editable: EditableMode, python: Option, settings: ResolverInstallerSettings, @@ -336,10 +336,10 @@ pub(crate) async fn run( if !extras.is_empty() { warn_user!("Extras are not supported for Python scripts with inline metadata"); } - if matches!(dev, DevMode::Exclude) { + if matches!(dev.dev_mode(), Some(DevMode::Exclude)) { warn_user!("`--no-dev` is not supported for Python scripts with inline metadata"); } - if matches!(dev, DevMode::Only) { + if matches!(dev.dev_mode(), Some(DevMode::Only)) { warn_user!("`--only-dev` is not supported for Python scripts with inline metadata"); } if package.is_some() { @@ -413,10 +413,10 @@ pub(crate) async fn run( if !extras.is_empty() { warn_user!("Extras have no effect when used alongside `--no-project`"); } - if matches!(dev, DevMode::Exclude) { + if matches!(dev.dev_mode(), Some(DevMode::Exclude)) { warn_user!("`--no-dev` has no effect when used alongside `--no-project`"); } - if matches!(dev, DevMode::Only) { + if matches!(dev.dev_mode(), Some(DevMode::Only)) { warn_user!("`--only-dev` has no effect when used alongside `--no-project`"); } if locked { @@ -433,10 +433,10 @@ pub(crate) async fn run( if !extras.is_empty() { warn_user!("Extras have no effect when used outside of a project"); } - if matches!(dev, DevMode::Exclude) { + if matches!(dev.dev_mode(), Some(DevMode::Exclude)) { warn_user!("`--no-dev` has no effect when used outside of a project"); } - if matches!(dev, DevMode::Only) { + if matches!(dev.dev_mode(), Some(DevMode::Only)) { warn_user!("`--only-dev` has no effect when used outside of a project"); } if locked { @@ -590,7 +590,7 @@ pub(crate) async fn run( &venv, result.lock(), &extras, - &DevSpecification::from(dev), + &dev.with_default_dev(), editable, install_options, Modifications::Sufficient, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 6f60a888b009..0d0e157fa054 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -8,7 +8,7 @@ use uv_auth::store_credentials; use uv_cache::Cache; use uv_client::{Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ - Concurrency, Constraints, DevSpecification, EditableMode, ExtrasSpecification, + Concurrency, Constraints, DevGroupsSpecification, EditableMode, ExtrasSpecification, HashCheckingMode, InstallOptions, LowerBound, }; use uv_dispatch::BuildDispatch; @@ -43,7 +43,7 @@ pub(crate) async fn sync( frozen: bool, package: Option, extras: ExtrasSpecification, - dev: DevSpecification, + dev: DevGroupsSpecification, editable: EditableMode, install_options: InstallOptions, modifications: Modifications, @@ -154,7 +154,7 @@ pub(crate) async fn sync( &venv, &lock, &extras, - &dev, + &dev.with_default_dev(), editable, install_options, modifications, @@ -178,7 +178,7 @@ pub(super) async fn do_sync( venv: &PythonEnvironment, lock: &Lock, extras: &ExtrasSpecification, - dev: &DevSpecification, + dev: &DevGroupsSpecification, editable: EditableMode, install_options: InstallOptions, modifications: Modifications, diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs index c767aca51fc7..6c48fb82ca0c 100644 --- a/crates/uv/src/commands/project/tree.rs +++ b/crates/uv/src/commands/project/tree.rs @@ -5,7 +5,7 @@ use anyhow::Result; use uv_cache::Cache; use uv_client::Connectivity; -use uv_configuration::{Concurrency, DevMode, LowerBound, TargetTriple}; +use uv_configuration::{Concurrency, DevGroupsSpecification, LowerBound, TargetTriple}; use uv_pep508::PackageName; use uv_python::{PythonDownloads, PythonPreference, PythonRequest, PythonVersion}; use uv_resolver::TreeDisplay; @@ -22,7 +22,7 @@ use crate::settings::ResolverSettings; #[allow(clippy::fn_params_excessive_bools)] pub(crate) async fn tree( project_dir: &Path, - dev: DevMode, + dev: DevGroupsSpecification, locked: bool, frozen: bool, universal: bool, @@ -96,7 +96,7 @@ pub(crate) async fn tree( depth.into(), prune, package, - dev, + dev.with_default_dev(), no_dedupe, invert, ); diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 04b4995a7da4..6f84b8f4ddc6 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -19,10 +19,10 @@ use uv_cli::{ }; use uv_client::Connectivity; use uv_configuration::{ - BuildOptions, Concurrency, ConfigSettings, DevMode, DevSpecification, EditableMode, - ExportFormat, ExtrasSpecification, HashCheckingMode, IndexStrategy, InstallOptions, - KeyringProviderType, NoBinary, NoBuild, PreviewMode, ProjectBuildBackend, Reinstall, - SourceStrategy, TargetTriple, TrustedHost, TrustedPublishing, Upgrade, VersionControlSystem, + BuildOptions, Concurrency, ConfigSettings, DevGroupsSpecification, EditableMode, ExportFormat, + ExtrasSpecification, HashCheckingMode, IndexStrategy, InstallOptions, KeyringProviderType, + NoBinary, NoBuild, PreviewMode, ProjectBuildBackend, Reinstall, SourceStrategy, TargetTriple, + TrustedHost, TrustedPublishing, Upgrade, VersionControlSystem, }; use uv_distribution_types::{DependencyMetadata, Index, IndexLocations}; use uv_install_wheel::linker::LinkMode; @@ -227,7 +227,7 @@ pub(crate) struct RunSettings { pub(crate) locked: bool, pub(crate) frozen: bool, pub(crate) extras: ExtrasSpecification, - pub(crate) dev: DevMode, + pub(crate) dev: DevGroupsSpecification, pub(crate) editable: EditableMode, pub(crate) with: Vec, pub(crate) with_editable: Vec, @@ -280,7 +280,8 @@ impl RunSettings { flag(all_extras, no_all_extras).unwrap_or_default(), extra.unwrap_or_default(), ), - dev: DevMode::from_args(dev, no_dev, only_dev), + // TODO(zanieb): Support `--group` here + dev: DevGroupsSpecification::from_args(dev, no_dev, only_dev, vec![], vec![]), editable: EditableMode::from_args(no_editable), with, with_editable, @@ -690,7 +691,7 @@ pub(crate) struct SyncSettings { pub(crate) locked: bool, pub(crate) frozen: bool, pub(crate) extras: ExtrasSpecification, - pub(crate) dev: DevSpecification, + pub(crate) dev: DevGroupsSpecification, pub(crate) editable: EditableMode, pub(crate) install_options: InstallOptions, pub(crate) modifications: Modifications, @@ -740,7 +741,7 @@ impl SyncSettings { flag(all_extras, no_all_extras).unwrap_or_default(), extra.unwrap_or_default(), ), - dev: DevSpecification::from_args(dev, no_dev, only_dev, group, only_group), + dev: DevGroupsSpecification::from_args(dev, no_dev, only_dev, group, only_group), editable: EditableMode::from_args(no_editable), install_options: InstallOptions::new( no_install_project, @@ -995,7 +996,7 @@ impl RemoveSettings { #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct TreeSettings { - pub(crate) dev: DevMode, + pub(crate) dev: DevGroupsSpecification, pub(crate) locked: bool, pub(crate) frozen: bool, pub(crate) universal: bool, @@ -1028,7 +1029,8 @@ impl TreeSettings { } = args; Self { - dev: DevMode::from_args(dev, no_dev, false), + // TODO(zanieb): Support `--group` here + dev: DevGroupsSpecification::from_args(dev, no_dev, false, vec![], vec![]), locked, frozen, universal, @@ -1052,7 +1054,7 @@ pub(crate) struct ExportSettings { pub(crate) format: ExportFormat, pub(crate) package: Option, pub(crate) extras: ExtrasSpecification, - pub(crate) dev: DevMode, + pub(crate) dev: DevGroupsSpecification, pub(crate) editable: EditableMode, pub(crate) hashes: bool, pub(crate) install_options: InstallOptions, @@ -1102,7 +1104,8 @@ impl ExportSettings { flag(all_extras, no_all_extras).unwrap_or_default(), extra.unwrap_or_default(), ), - dev: DevMode::from_args(dev, no_dev, only_dev), + // TODO(zanieb): Support `--group` here + dev: DevGroupsSpecification::from_args(dev, no_dev, only_dev, vec![], vec![]), editable: EditableMode::from_args(no_editable), hashes: flag(hashes, no_hashes).unwrap_or(true), install_options: InstallOptions::new(