mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-03 05:03:46 +00:00
Add --no-group support to CLI (#8477)
## Summary Now that `default-groups` can include more than just `"dev"`, it makes sense to allow users to remove groups with `--no-group`.
This commit is contained in:
parent
291c4c496d
commit
810b430031
7 changed files with 278 additions and 26 deletions
|
|
@ -2617,6 +2617,12 @@ pub struct RunArgs {
|
|||
#[arg(long, conflicts_with("only_group"))]
|
||||
pub group: Vec<GroupName>,
|
||||
|
||||
/// Exclude dependencies from the specified local dependency group.
|
||||
///
|
||||
/// May be provided multiple times.
|
||||
#[arg(long)]
|
||||
pub no_group: Vec<GroupName>,
|
||||
|
||||
/// Only include dependencies from the specified local dependency group.
|
||||
///
|
||||
/// May be provided multiple times.
|
||||
|
|
@ -2808,6 +2814,12 @@ pub struct SyncArgs {
|
|||
#[arg(long, conflicts_with("only_group"))]
|
||||
pub group: Vec<GroupName>,
|
||||
|
||||
/// Exclude dependencies from the specified local dependency group.
|
||||
///
|
||||
/// May be provided multiple times.
|
||||
#[arg(long)]
|
||||
pub no_group: Vec<GroupName>,
|
||||
|
||||
/// Only include dependencies from the specified local dependency group.
|
||||
///
|
||||
/// May be provided multiple times.
|
||||
|
|
@ -3205,6 +3217,12 @@ pub struct TreeArgs {
|
|||
#[arg(long, conflicts_with("only_group"))]
|
||||
pub group: Vec<GroupName>,
|
||||
|
||||
/// Exclude dependencies from the specified local dependency group.
|
||||
///
|
||||
/// May be provided multiple times.
|
||||
#[arg(long)]
|
||||
pub no_group: Vec<GroupName>,
|
||||
|
||||
/// Only include dependencies from the specified local dependency group.
|
||||
///
|
||||
/// May be provided multiple times.
|
||||
|
|
@ -3320,6 +3338,12 @@ pub struct ExportArgs {
|
|||
#[arg(long, conflicts_with("only_group"))]
|
||||
pub group: Vec<GroupName>,
|
||||
|
||||
/// Exclude dependencies from the specified local dependency group.
|
||||
///
|
||||
/// May be provided multiple times.
|
||||
#[arg(long)]
|
||||
pub no_group: Vec<GroupName>,
|
||||
|
||||
/// Only include dependencies from the specified local dependency group.
|
||||
///
|
||||
/// May be provided multiple times.
|
||||
|
|
|
|||
|
|
@ -68,38 +68,76 @@ pub struct DevGroupsSpecification {
|
|||
#[derive(Debug, Clone)]
|
||||
pub enum GroupsSpecification {
|
||||
/// Include dependencies from the specified groups.
|
||||
Include(Vec<GroupName>),
|
||||
///
|
||||
/// The `include` list is guaranteed to omit groups in the `exclude` list (i.e., they have an
|
||||
/// empty intersection).
|
||||
Include {
|
||||
include: Vec<GroupName>,
|
||||
exclude: Vec<GroupName>,
|
||||
},
|
||||
/// Only include dependencies from the specified groups, exclude all other dependencies.
|
||||
Only(Vec<GroupName>),
|
||||
///
|
||||
/// The `include` list is guaranteed to omit groups in the `exclude` list (i.e., they have an
|
||||
/// empty intersection).
|
||||
Only {
|
||||
include: Vec<GroupName>,
|
||||
exclude: Vec<GroupName>,
|
||||
},
|
||||
}
|
||||
|
||||
impl GroupsSpecification {
|
||||
/// Create a [`GroupsSpecification`] that includes the given group.
|
||||
pub fn from_group(group: GroupName) -> Self {
|
||||
Self::Include {
|
||||
include: vec![group],
|
||||
exclude: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the specification allows for production dependencies.
|
||||
pub fn prod(&self) -> bool {
|
||||
matches!(self, Self::Include(_))
|
||||
matches!(self, Self::Include { .. })
|
||||
}
|
||||
|
||||
/// Returns `true` if the specification is limited to a select set of groups.
|
||||
pub fn only(&self) -> bool {
|
||||
matches!(self, Self::Only(_))
|
||||
matches!(self, Self::Only { .. })
|
||||
}
|
||||
|
||||
/// Returns the option that was used to request the groups, if any.
|
||||
pub fn as_flag(&self) -> Option<Cow<'_, str>> {
|
||||
match self {
|
||||
Self::Include(groups) => match groups.as_slice() {
|
||||
[] => None,
|
||||
Self::Include { 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!("--group {group}"))),
|
||||
[..] => Some(Cow::Borrowed("--group")),
|
||||
},
|
||||
Self::Only(groups) => match groups.as_slice() {
|
||||
[] => None,
|
||||
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")),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over all groups referenced in the [`DevGroupsSpecification`].
|
||||
pub fn names(&self) -> impl Iterator<Item = &GroupName> {
|
||||
match self {
|
||||
GroupsSpecification::Include { include, exclude }
|
||||
| GroupsSpecification::Only { include, exclude } => {
|
||||
include.iter().chain(exclude.iter())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over the group names to include.
|
||||
pub fn iter(&self) -> impl Iterator<Item = &GroupName> {
|
||||
<&Self as IntoIterator>::into_iter(self)
|
||||
|
|
@ -112,9 +150,14 @@ impl<'a> IntoIterator for &'a GroupsSpecification {
|
|||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
match self {
|
||||
GroupsSpecification::Include(groups) | GroupsSpecification::Only(groups) => {
|
||||
groups.iter()
|
||||
GroupsSpecification::Include {
|
||||
include,
|
||||
exclude: _,
|
||||
}
|
||||
| GroupsSpecification::Only {
|
||||
include,
|
||||
exclude: _,
|
||||
} => include.iter(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -125,8 +168,9 @@ impl DevGroupsSpecification {
|
|||
dev: bool,
|
||||
no_dev: bool,
|
||||
only_dev: bool,
|
||||
group: Vec<GroupName>,
|
||||
only_group: Vec<GroupName>,
|
||||
mut group: Vec<GroupName>,
|
||||
no_group: Vec<GroupName>,
|
||||
mut only_group: Vec<GroupName>,
|
||||
) -> Self {
|
||||
let dev = if only_dev {
|
||||
Some(DevMode::Only)
|
||||
|
|
@ -142,12 +186,31 @@ impl DevGroupsSpecification {
|
|||
if matches!(dev, Some(DevMode::Only)) {
|
||||
unreachable!("cannot specify both `--only-dev` and `--group`")
|
||||
};
|
||||
Some(GroupsSpecification::Include(group))
|
||||
|
||||
// Ensure that `--no-group` and `--group` are mutually exclusive.
|
||||
group.retain(|group| !no_group.contains(group));
|
||||
|
||||
Some(GroupsSpecification::Include {
|
||||
include: group,
|
||||
exclude: no_group,
|
||||
})
|
||||
} else if !only_group.is_empty() {
|
||||
if matches!(dev, Some(DevMode::Include)) {
|
||||
unreachable!("cannot specify both `--dev` and `--only-group`")
|
||||
};
|
||||
Some(GroupsSpecification::Only(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: Vec::new(),
|
||||
exclude: no_group,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
|
@ -270,8 +333,24 @@ impl DevGroupsManifest {
|
|||
.iter()
|
||||
.chain(self.defaults.iter().filter(|default| {
|
||||
// If `--no-dev` was provided, exclude the `dev` group from the list of defaults.
|
||||
!matches!(self.spec.dev_mode(), Some(DevMode::Exclude))
|
||||
|| *default != &*DEV_DEPENDENCIES
|
||||
if matches!(self.spec.dev_mode(), Some(DevMode::Exclude)) {
|
||||
if *default == &*DEV_DEPENDENCIES {
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
// 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
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -827,9 +827,7 @@ async fn lock_and_sync(
|
|||
DependencyType::Group(ref group_name) => {
|
||||
let extras = ExtrasSpecification::None;
|
||||
let dev =
|
||||
DevGroupsSpecification::from(GroupsSpecification::Include(
|
||||
vec![group_name.clone()],
|
||||
));
|
||||
DevGroupsSpecification::from(GroupsSpecification::from_group(group_name.clone()));
|
||||
(extras, dev)
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ use tracing::debug;
|
|||
use uv_cache::Cache;
|
||||
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
|
||||
use uv_configuration::{
|
||||
Concurrency, Constraints, DevGroupsSpecification, ExtrasSpecification, LowerBound, Reinstall,
|
||||
Upgrade,
|
||||
Concurrency, Constraints, DevGroupsSpecification, ExtrasSpecification, GroupsSpecification,
|
||||
LowerBound, Reinstall, Upgrade,
|
||||
};
|
||||
use uv_dispatch::BuildDispatch;
|
||||
use uv_distribution::DistributionDatabase;
|
||||
|
|
@ -1370,7 +1370,11 @@ pub(crate) fn validate_dependency_groups(
|
|||
pyproject_toml: &PyProjectToml,
|
||||
dev: &DevGroupsSpecification,
|
||||
) -> Result<(), ProjectError> {
|
||||
for group in dev.groups().into_iter().flatten() {
|
||||
for group in dev
|
||||
.groups()
|
||||
.into_iter()
|
||||
.flat_map(GroupsSpecification::names)
|
||||
{
|
||||
if !pyproject_toml
|
||||
.dependency_groups
|
||||
.as_ref()
|
||||
|
|
|
|||
|
|
@ -253,6 +253,7 @@ impl RunSettings {
|
|||
dev,
|
||||
no_dev,
|
||||
group,
|
||||
no_group,
|
||||
only_group,
|
||||
module: _,
|
||||
only_dev,
|
||||
|
|
@ -282,7 +283,9 @@ impl RunSettings {
|
|||
flag(all_extras, no_all_extras).unwrap_or_default(),
|
||||
extra.unwrap_or_default(),
|
||||
),
|
||||
dev: DevGroupsSpecification::from_args(dev, no_dev, only_dev, group, only_group),
|
||||
dev: DevGroupsSpecification::from_args(
|
||||
dev, no_dev, only_dev, group, no_group, only_group,
|
||||
),
|
||||
editable: EditableMode::from_args(no_editable),
|
||||
with,
|
||||
with_editable,
|
||||
|
|
@ -718,6 +721,7 @@ impl SyncSettings {
|
|||
only_dev,
|
||||
group,
|
||||
only_group,
|
||||
no_group,
|
||||
no_editable,
|
||||
inexact,
|
||||
exact,
|
||||
|
|
@ -745,7 +749,9 @@ impl SyncSettings {
|
|||
flag(all_extras, no_all_extras).unwrap_or_default(),
|
||||
extra.unwrap_or_default(),
|
||||
),
|
||||
dev: DevGroupsSpecification::from_args(dev, no_dev, only_dev, group, only_group),
|
||||
dev: DevGroupsSpecification::from_args(
|
||||
dev, no_dev, only_dev, group, no_group, only_group,
|
||||
),
|
||||
editable: EditableMode::from_args(no_editable),
|
||||
install_options: InstallOptions::new(
|
||||
no_install_project,
|
||||
|
|
@ -1028,6 +1034,7 @@ impl TreeSettings {
|
|||
only_dev,
|
||||
no_dev,
|
||||
group,
|
||||
no_group,
|
||||
only_group,
|
||||
locked,
|
||||
frozen,
|
||||
|
|
@ -1039,7 +1046,9 @@ impl TreeSettings {
|
|||
} = args;
|
||||
|
||||
Self {
|
||||
dev: DevGroupsSpecification::from_args(dev, no_dev, only_dev, group, only_group),
|
||||
dev: DevGroupsSpecification::from_args(
|
||||
dev, no_dev, only_dev, group, no_group, only_group,
|
||||
),
|
||||
locked,
|
||||
frozen,
|
||||
universal,
|
||||
|
|
@ -1090,6 +1099,7 @@ impl ExportSettings {
|
|||
no_dev,
|
||||
only_dev,
|
||||
group,
|
||||
no_group,
|
||||
only_group,
|
||||
header,
|
||||
no_header,
|
||||
|
|
@ -1115,7 +1125,9 @@ impl ExportSettings {
|
|||
flag(all_extras, no_all_extras).unwrap_or_default(),
|
||||
extra.unwrap_or_default(),
|
||||
),
|
||||
dev: DevGroupsSpecification::from_args(dev, no_dev, only_dev, group, only_group),
|
||||
dev: DevGroupsSpecification::from_args(
|
||||
dev, no_dev, only_dev, group, no_group, only_group,
|
||||
),
|
||||
editable: EditableMode::from_args(no_editable),
|
||||
hashes: flag(hashes, no_hashes).unwrap_or(true),
|
||||
install_options: InstallOptions::new(
|
||||
|
|
|
|||
|
|
@ -1184,6 +1184,84 @@ fn sync_include_group() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_exclude_group() -> Result<()> {
|
||||
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]
|
||||
foo = ["anyio", {include-group = "bar"}]
|
||||
bar = ["iniconfig"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
context.lock().assert().success();
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 6 packages in [TIME]
|
||||
Prepared 5 packages in [TIME]
|
||||
Installed 5 packages in [TIME]
|
||||
+ anyio==4.3.0
|
||||
+ idna==3.6
|
||||
+ iniconfig==2.0.0
|
||||
+ sniffio==1.3.1
|
||||
+ typing-extensions==4.10.0
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo").arg("--no-group").arg("foo"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 6 packages in [TIME]
|
||||
Uninstalled 4 packages in [TIME]
|
||||
- anyio==4.3.0
|
||||
- idna==3.6
|
||||
- iniconfig==2.0.0
|
||||
- sniffio==1.3.1
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("bar"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 6 packages in [TIME]
|
||||
Uninstalled 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ iniconfig==2.0.0
|
||||
- typing-extensions==4.10.0
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("bar").arg("--no-group").arg("bar"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 6 packages in [TIME]
|
||||
Uninstalled 1 package in [TIME]
|
||||
- iniconfig==2.0.0
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sync_dev_group() -> Result<()> {
|
||||
let context = TestContext::new("3.12");
|
||||
|
|
@ -1257,6 +1335,15 @@ fn sync_non_existent_group() -> Result<()> {
|
|||
error: Group `baz` is not defined in the project's `dependency-group` table
|
||||
"###);
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--no-group").arg("baz"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Group `baz` is not defined in the project's `dependency-group` table
|
||||
"###);
|
||||
|
||||
// Requesting an empty group should succeed.
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--group").arg("foo"), @r###"
|
||||
success: true
|
||||
|
|
@ -1407,6 +1494,38 @@ fn sync_default_groups() -> Result<()> {
|
|||
+ sniffio==1.3.1
|
||||
"###);
|
||||
|
||||
// `--no-group` should remove from the defaults.
|
||||
pyproject_toml.write_str(
|
||||
r#"
|
||||
[project]
|
||||
name = "project"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = ["typing-extensions"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["iniconfig"]
|
||||
foo = ["anyio"]
|
||||
bar = ["requests"]
|
||||
|
||||
[tool.uv]
|
||||
default-groups = ["foo"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--no-group").arg("foo"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Resolved 10 packages in [TIME]
|
||||
Uninstalled 3 packages in [TIME]
|
||||
- anyio==4.3.0
|
||||
- idna==3.6
|
||||
- sniffio==1.3.1
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -286,6 +286,10 @@ uv run [OPTIONS] [COMMAND]
|
|||
|
||||
</dd><dt><code>--no-editable</code></dt><dd><p>Install any editable dependencies, including the project and any workspace members, as non-editable</p>
|
||||
|
||||
</dd><dt><code>--no-group</code> <i>no-group</i></dt><dd><p>Exclude dependencies from the specified local dependency group.</p>
|
||||
|
||||
<p>May be provided multiple times.</p>
|
||||
|
||||
</dd><dt><code>--no-index</code></dt><dd><p>Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those provided via <code>--find-links</code></p>
|
||||
|
||||
</dd><dt><code>--no-progress</code></dt><dd><p>Hide all progress outputs.</p>
|
||||
|
|
@ -1539,6 +1543,10 @@ uv sync [OPTIONS]
|
|||
|
||||
</dd><dt><code>--no-editable</code></dt><dd><p>Install any editable dependencies, including the project and any workspace members, as non-editable</p>
|
||||
|
||||
</dd><dt><code>--no-group</code> <i>no-group</i></dt><dd><p>Exclude dependencies from the specified local dependency group.</p>
|
||||
|
||||
<p>May be provided multiple times.</p>
|
||||
|
||||
</dd><dt><code>--no-index</code></dt><dd><p>Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those provided via <code>--find-links</code></p>
|
||||
|
||||
</dd><dt><code>--no-install-package</code> <i>no-install-package</i></dt><dd><p>Do not install the given package(s).</p>
|
||||
|
|
@ -2189,6 +2197,10 @@ uv export [OPTIONS]
|
|||
|
||||
<p>By default, all workspace members and their dependencies are included in the exported requirements file, with all of their dependencies. The <code>--no-emit-workspace</code> option allows exclusion of all the workspace members while retaining their dependencies.</p>
|
||||
|
||||
</dd><dt><code>--no-group</code> <i>no-group</i></dt><dd><p>Exclude dependencies from the specified local dependency group.</p>
|
||||
|
||||
<p>May be provided multiple times.</p>
|
||||
|
||||
</dd><dt><code>--no-hashes</code></dt><dd><p>Omit hashes in the generated output</p>
|
||||
|
||||
</dd><dt><code>--no-header</code></dt><dd><p>Exclude the comment header at the top of the generated output file</p>
|
||||
|
|
@ -2506,6 +2518,10 @@ uv tree [OPTIONS]
|
|||
|
||||
</dd><dt><code>--no-dev</code></dt><dd><p>Omit development dependencies</p>
|
||||
|
||||
</dd><dt><code>--no-group</code> <i>no-group</i></dt><dd><p>Exclude dependencies from the specified local dependency group.</p>
|
||||
|
||||
<p>May be provided multiple times.</p>
|
||||
|
||||
</dd><dt><code>--no-index</code></dt><dd><p>Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those provided via <code>--find-links</code></p>
|
||||
|
||||
</dd><dt><code>--no-progress</code></dt><dd><p>Hide all progress outputs.</p>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue