Skip to content
Merged
84 changes: 76 additions & 8 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,40 @@ pub struct PipCompileArgs {
#[arg(long, overrides_with("all_extras"), hide = true)]
pub no_all_extras: bool,

/// Include dependencies from the specified dependency group.
///
/// Only applies to `pyproject.toml` sources.
///
/// May be provided multiple times.
#[arg(long, conflicts_with("only_group"))]
pub group: Vec<GroupName>,

/// Exclude dependencies from the specified dependency group.
///
/// Only applies to `pyproject.toml` sources.
///
/// May be provided multiple times.
#[arg(long)]
pub no_group: Vec<GroupName>,

/// Only include dependencies from the specified dependency group.
///
/// The project itself will also be omitted.
///
/// Only applies to `pyproject.toml` sources.
///
/// May be provided multiple times.
#[arg(long, conflicts_with("group"))]
pub only_group: Vec<GroupName>,

/// Include dependencies from all dependency groups.
///
/// Only applies to `pyproject.toml` sources.
///
/// `--no-group` can be used to exclude specific groups.
#[arg(long, conflicts_with_all = [ "group", "only_group" ])]
pub all_groups: bool,

#[command(flatten)]
pub resolver: ResolverArgs,

Expand Down Expand Up @@ -1572,6 +1606,40 @@ pub struct PipInstallArgs {
#[arg(long, overrides_with("all_extras"), hide = true)]
pub no_all_extras: bool,

/// Include dependencies from the specified dependency group.
///
/// Only applies to `pyproject.toml` sources.
///
/// May be provided multiple times.
#[arg(long, conflicts_with("only_group"))]
pub group: Vec<GroupName>,

/// Exclude dependencies from the specified dependency group.
///
/// Only applies to `pyproject.toml` sources.
///
/// May be provided multiple times.
#[arg(long)]
pub no_group: Vec<GroupName>,

/// Only include dependencies from the specified dependency group.
///
/// The project itself will also be omitted.
///
/// Only applies to `pyproject.toml` sources.
///
/// May be provided multiple times.
#[arg(long, conflicts_with("group"))]
pub only_group: Vec<GroupName>,

/// Include dependencies from all dependency groups.
///
/// Only applies to `pyproject.toml` sources.
///
/// `--no-group` can be used to exclude specific groups.
#[arg(long, conflicts_with_all = [ "group", "only_group" ])]
pub all_groups: bool,

#[command(flatten)]
pub installer: ResolverInstallerArgs,

Expand Down Expand Up @@ -2727,9 +2795,9 @@ pub struct RunArgs {

/// Only include dependencies from the specified dependency group.
///
/// May be provided multiple times.
///
/// The project itself will also be omitted.
///
/// May be provided multiple times.
#[arg(long, conflicts_with("group"))]
pub only_group: Vec<GroupName>,

Expand Down Expand Up @@ -2998,9 +3066,9 @@ pub struct SyncArgs {

/// Only include dependencies from the specified dependency group.
///
/// May be provided multiple times.
///
/// The project itself will also be omitted.
///
/// May be provided multiple times.
#[arg(long, conflicts_with("group"))]
pub only_group: Vec<GroupName>,

Expand Down Expand Up @@ -3449,9 +3517,9 @@ pub struct TreeArgs {

/// Only include dependencies from the specified dependency group.
///
/// May be provided multiple times.
///
/// The project itself will also be omitted.
///
/// May be provided multiple times.
#[arg(long, conflicts_with("group"))]
pub only_group: Vec<GroupName>,

Expand Down Expand Up @@ -3619,9 +3687,9 @@ pub struct ExportArgs {

/// Only include dependencies from the specified dependency group.
///
/// May be provided multiple times.
///
/// The project itself will also be omitted.
///
/// May be provided multiple times.
#[arg(long, conflicts_with("group"))]
pub only_group: Vec<GroupName>,

Expand Down
25 changes: 24 additions & 1 deletion crates/uv-configuration/src/dev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ impl GroupsSpecification {
}
}

/// Iterate over all groups referenced in the [`DevGroupsSpecification`].
/// Iterate over all groups referenced in the [`GroupsSpecification`].
pub fn names(&self) -> impl Iterator<Item = &GroupName> {
match self {
GroupsSpecification::Include { include, exclude } => {
Expand All @@ -157,6 +157,18 @@ impl GroupsSpecification {
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()
}
}
Comment on lines +161 to 172
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely sure about the correctness of this. Is this relying on some assumptions about the construction of GroupsSpecification?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is, but it's a relatively mundane assumption imo.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth a comment saying what the assumption is, but don't feel strongly.


#[derive(Debug, Clone)]
Expand Down Expand Up @@ -316,6 +328,17 @@ impl DevGroupsSpecification {
.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<DevMode> for DevGroupsSpecification {
Expand Down
32 changes: 28 additions & 4 deletions crates/uv-requirements/src/source_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use futures::stream::FuturesOrdered;
use futures::TryStreamExt;
use url::Url;

use uv_configuration::ExtrasSpecification;
use uv_configuration::{DevGroupsSpecification, ExtrasSpecification};
use uv_distribution::{DistributionDatabase, FlatRequiresDist, Reporter, RequiresDist};
use uv_distribution_types::{
BuildableSource, DirectorySourceUrl, HashGeneration, HashPolicy, SourceUrl, VersionId,
Expand All @@ -18,7 +18,7 @@ use uv_pep508::RequirementOrigin;
use uv_pypi_types::Requirement;
use uv_resolver::{InMemoryIndex, MetadataResponse};
use uv_types::{BuildContext, HashStrategy};

use uv_warnings::warn_user_once;
#[derive(Debug, Clone)]
pub struct SourceTreeResolution {
/// The requirements sourced from the source trees.
Expand All @@ -36,6 +36,8 @@ pub struct SourceTreeResolution {
pub struct SourceTreeResolver<'a, Context: BuildContext> {
/// The extras to include when resolving requirements.
extras: &'a ExtrasSpecification,
/// The groups to include when resolving requirements.
groups: &'a DevGroupsSpecification,
/// The hash policy to enforce.
hasher: &'a HashStrategy,
/// The in-memory index for resolving dependencies.
Expand All @@ -48,12 +50,14 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
/// Instantiate a new [`SourceTreeResolver`] for a given set of `source_trees`.
pub fn new(
extras: &'a ExtrasSpecification,
groups: &'a DevGroupsSpecification,
hasher: &'a HashStrategy,
index: &'a InMemoryIndex,
database: DistributionDatabase<'a, Context>,
) -> Self {
Self {
extras,
groups,
hasher,
index,
database,
Expand Down Expand Up @@ -85,7 +89,6 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
/// Infer the dependencies for a directory dependency.
async fn resolve_source_tree(&self, path: &Path) -> Result<SourceTreeResolution> {
let metadata = self.resolve_requires_dist(path).await?;

let origin = RequirementOrigin::Project(path.to_path_buf(), metadata.name.clone());

// Determine the extras to include when resolving the requirements.
Expand All @@ -96,7 +99,7 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
.collect::<Vec<_>>();

// Flatten any transitive extras.
let requirements =
let mut requirements =
FlatRequiresDist::from_requirements(metadata.requires_dist, &metadata.name)
.into_iter()
.map(|requirement| Requirement {
Expand All @@ -106,6 +109,27 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
})
.collect::<Vec<_>>();

// Apply dependency-groups
for (group_name, group) in &metadata.dependency_groups {
if self.groups.contains(group_name) {
requirements.extend(group.iter().cloned());
}
}
// Complain if dependency groups are named that don't appear.
// 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()
);
}
}
}

let project = metadata.name;
let extras = metadata.provides_extras;

Expand Down
5 changes: 5 additions & 0 deletions crates/uv-requirements/src/sources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,11 @@ impl RequirementsSource {
Self::PyprojectToml(_) | Self::SetupPy(_) | Self::SetupCfg(_)
)
}

/// Returns `true` if the source allows groups to be specified.
pub fn allows_groups(&self) -> bool {
matches!(self, Self::PyprojectToml(_))
}
}

impl std::fmt::Display for RequirementsSource {
Expand Down
46 changes: 45 additions & 1 deletion crates/uv-settings/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use uv_distribution_types::{
};
use uv_install_wheel::linker::LinkMode;
use uv_macros::{CombineOptions, OptionsMetadata};
use uv_normalize::{ExtraName, PackageName};
use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_pep508::Requirement;
use uv_pypi_types::{SupportedEnvironments, VerbatimParsedUrl};
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
Expand Down Expand Up @@ -1062,6 +1062,50 @@ pub struct PipOptions {
"#
)]
pub no_extra: Option<Vec<ExtraName>>,
/// Include optional dependencies from the specified group; may be provided more than once.
///
/// Only applies to `pyproject.toml` sources.
#[option(
default = "[]",
value_type = "list[str]",
example = r#"
group = ["dev", "docs"]
"#
)]
pub group: Option<Vec<GroupName>>,
/// Exclude optional dependencies from the specified group if `all-groups` are supplied
///
/// Only applies to `pyproject.toml` sources.
#[option(
default = "[]",
value_type = "list[str]",
example = r#"
no-group = ["dev", "docs"]
"#
)]
pub no_group: Option<Vec<GroupName>>,
/// Exclude only dependencies from the specified group.
///
/// Only applies to `pyproject.toml` sources.
#[option(
default = "[]",
value_type = "list[str]",
example = r#"
only-group = ["dev", "docs"]
"#
)]
pub only_group: Option<Vec<GroupName>>,
/// Include all groups.
///
/// Only applies to `pyproject.toml` sources.
#[option(
default = "false",
value_type = "bool",
example = r#"
all-groups = true
"#
)]
pub all_groups: Option<bool>,
/// Ignore package dependencies, instead only add those packages explicitly listed
/// on the command line to the resulting the requirements file.
#[option(
Expand Down
7 changes: 5 additions & 2 deletions crates/uv/src/commands/pip/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ use tracing::debug;
use uv_cache::Cache;
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
BuildOptions, Concurrency, ConfigSettings, Constraints, ExtrasSpecification, IndexStrategy,
LowerBound, NoBinary, NoBuild, PreviewMode, Reinstall, SourceStrategy, TrustedHost, Upgrade,
BuildOptions, Concurrency, ConfigSettings, Constraints, DevGroupsSpecification,
ExtrasSpecification, IndexStrategy, LowerBound, NoBinary, NoBuild, PreviewMode, Reinstall,
SourceStrategy, TrustedHost, Upgrade,
};
use uv_configuration::{KeyringProviderType, TargetTriple};
use uv_dispatch::{BuildDispatch, SharedState};
Expand Down Expand Up @@ -55,6 +56,7 @@ pub(crate) async fn pip_compile(
overrides_from_workspace: Vec<Requirement>,
environments: SupportedEnvironments,
extras: ExtrasSpecification,
groups: DevGroupsSpecification,
output_file: Option<&Path>,
resolution_mode: ResolutionMode,
prerelease_mode: PrereleaseMode,
Expand Down Expand Up @@ -379,6 +381,7 @@ pub(crate) async fn pip_compile(
project,
BTreeSet::default(),
&extras,
&groups,
preferences,
EmptyInstalledPackages,
&hasher,
Expand Down
8 changes: 6 additions & 2 deletions crates/uv/src/commands/pip/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ use tracing::{debug, enabled, Level};
use uv_cache::Cache;
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
BuildOptions, Concurrency, ConfigSettings, Constraints, ExtrasSpecification, HashCheckingMode,
IndexStrategy, LowerBound, PreviewMode, Reinstall, SourceStrategy, TrustedHost, Upgrade,
BuildOptions, Concurrency, ConfigSettings, Constraints, DevGroupsSpecification,
ExtrasSpecification, HashCheckingMode, IndexStrategy, LowerBound, PreviewMode, Reinstall,
SourceStrategy, TrustedHost, Upgrade,
};
use uv_configuration::{KeyringProviderType, TargetTriple};
use uv_dispatch::{BuildDispatch, SharedState};
Expand Down Expand Up @@ -50,6 +51,7 @@ pub(crate) async fn pip_install(
constraints_from_workspace: Vec<Requirement>,
overrides_from_workspace: Vec<Requirement>,
extras: &ExtrasSpecification,
groups: &DevGroupsSpecification,
resolution_mode: ResolutionMode,
prerelease_mode: PrereleaseMode,
dependency_mode: DependencyMode,
Expand Down Expand Up @@ -115,6 +117,7 @@ pub(crate) async fn pip_install(
constraints,
overrides,
extras,
groups,
&client_builder,
)
.await?;
Expand Down Expand Up @@ -406,6 +409,7 @@ pub(crate) async fn pip_install(
project,
BTreeSet::default(),
extras,
groups,
preferences,
site_packages.clone(),
&hasher,
Expand Down
Loading
Loading