Add conflict detection between --only-group and --extra flags (#15788)

<!--
Thank you for contributing to uv! To help us out with reviewing, please
consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

- Added `conflicts_with = "only_group"` to `--extra` arguments in
`SyncArgs`, `RunArgs`, and `ExportArgs`
- Added tests to verify proper conflict detection and error messages

**Before:** The `--extra` flag was silently ignored when used with
`--only-group`
**After:** Clear error message: `error: the argument '--only-group
<ONLY_GROUP>' cannot be used with '--extra <EXTRA>'`

fixes: #15676 

## Test Plan

- Tests confirm proper error message format when `--only-group` and
`--extra` are used together
- Verified existing functionality remains unchanged when flags are used
independently
This commit is contained in:
Harshith VH 2025-09-11 21:04:49 +05:30 committed by GitHub
parent 5f2871e695
commit a0f8359012
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 162 additions and 6 deletions

View File

@ -3077,7 +3077,7 @@ pub struct RunArgs {
/// Optional dependencies are defined via `project.optional-dependencies` in a `pyproject.toml`.
///
/// This option is only available when running in a project.
#[arg(long, conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)]
#[arg(long, conflicts_with = "all_extras", conflicts_with = "only_group", value_parser = extra_name_with_clap_error)]
pub extra: Option<Vec<ExtraName>>,
/// Include all optional dependencies.
@ -3085,7 +3085,7 @@ pub struct RunArgs {
/// Optional dependencies are defined via `project.optional-dependencies` in a `pyproject.toml`.
///
/// This option is only available when running in a project.
#[arg(long, conflicts_with = "extra")]
#[arg(long, conflicts_with = "extra", conflicts_with = "only_group")]
pub all_extras: bool,
/// Exclude the specified optional dependencies, if `--all-extras` is supplied.
@ -3396,7 +3396,7 @@ pub struct SyncArgs {
///
/// Note that all optional dependencies are always included in the resolution; this option only
/// affects the selection of packages to install.
#[arg(long, conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)]
#[arg(long, conflicts_with = "all_extras", conflicts_with = "only_group", value_parser = extra_name_with_clap_error)]
pub extra: Option<Vec<ExtraName>>,
/// Select the output format.
@ -3410,7 +3410,7 @@ pub struct SyncArgs {
///
/// Note that all optional dependencies are always included in the resolution; this option only
/// affects the selection of packages to install.
#[arg(long, conflicts_with = "extra")]
#[arg(long, conflicts_with = "extra", conflicts_with = "only_group")]
pub all_extras: bool,
/// Exclude the specified optional dependencies, if `--all-extras` is supplied.
@ -4247,11 +4247,11 @@ pub struct ExportArgs {
/// Include optional dependencies from the specified extra name.
///
/// May be provided more than once.
#[arg(long, conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)]
#[arg(long, conflicts_with = "all_extras", conflicts_with = "only_group", value_parser = extra_name_with_clap_error)]
pub extra: Option<Vec<ExtraName>>,
/// Include all optional dependencies.
#[arg(long, conflicts_with = "extra")]
#[arg(long, conflicts_with = "extra", conflicts_with = "only_group")]
pub all_extras: bool,
/// Exclude the specified optional dependencies, if `--all-extras` is supplied.

View File

@ -4467,3 +4467,55 @@ fn no_editable_env_var() -> Result<()> {
Ok(())
}
#[test]
fn export_only_group_and_extra_conflict() -> 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 = []
[project.optional-dependencies]
test = ["pytest"]
[dependency-groups]
dev = ["ruff"]
"#,
)?;
// Using --only-group and --extra together should error.
uv_snapshot!(context.filters(), context.export().arg("--only-group").arg("dev").arg("--extra").arg("test"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: the argument '--only-group <ONLY_GROUP>' cannot be used with '--extra <EXTRA>'
Usage: uv export --cache-dir [CACHE_DIR] --only-group <ONLY_GROUP> --exclude-newer <EXCLUDE_NEWER>
For more information, try '--help'.
"###);
// Using --only-group and --all-extras together should also error.
uv_snapshot!(context.filters(), context.export().arg("--only-group").arg("dev").arg("--all-extras"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: the argument '--only-group <ONLY_GROUP>' cannot be used with '--all-extras'
Usage: uv export --cache-dir [CACHE_DIR] --only-group <ONLY_GROUP> --exclude-newer <EXCLUDE_NEWER>
For more information, try '--help'.
"###);
Ok(())
}

View File

@ -6039,3 +6039,55 @@ fn isolate_child_environment() -> Result<()> {
Ok(())
}
#[test]
fn run_only_group_and_extra_conflict() -> 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 = []
[project.optional-dependencies]
test = ["pytest"]
[dependency-groups]
dev = ["ruff"]
"#,
)?;
// Using --only-group and --extra together should error.
uv_snapshot!(context.filters(), context.run().arg("--only-group").arg("dev").arg("--extra").arg("test").arg("python").arg("-c").arg("print('hello')"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: the argument '--only-group <ONLY_GROUP>' cannot be used with '--extra <EXTRA>'
Usage: uv run --cache-dir [CACHE_DIR] --only-group <ONLY_GROUP> --exclude-newer <EXCLUDE_NEWER>
For more information, try '--help'.
"###);
// Using --only-group and --all-extras together should also error.
uv_snapshot!(context.filters(), context.run().arg("--only-group").arg("dev").arg("--all-extras").arg("python").arg("-c").arg("print('hello')"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: the argument '--only-group <ONLY_GROUP>' cannot be used with '--all-extras'
Usage: uv run --cache-dir [CACHE_DIR] --only-group <ONLY_GROUP> --exclude-newer <EXCLUDE_NEWER>
For more information, try '--help'.
"###);
Ok(())
}

View File

@ -14093,3 +14093,55 @@ fn workspace_editable_conflict() -> Result<()> {
Ok(())
}
#[test]
fn only_group_and_extra_conflict() -> 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 = []
[project.optional-dependencies]
test = ["pytest"]
[dependency-groups]
dev = ["ruff"]
"#,
)?;
// Using --only-group and --extra together should error.
uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("dev").arg("--extra").arg("test"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: the argument '--only-group <ONLY_GROUP>' cannot be used with '--extra <EXTRA>'
Usage: uv sync --cache-dir [CACHE_DIR] --only-group <ONLY_GROUP> --exclude-newer <EXCLUDE_NEWER>
For more information, try '--help'.
"###);
// Using --only-group and --all-extras together should also error.
uv_snapshot!(context.filters(), context.sync().arg("--only-group").arg("dev").arg("--all-extras"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: the argument '--only-group <ONLY_GROUP>' cannot be used with '--all-extras'
Usage: uv sync --cache-dir [CACHE_DIR] --only-group <ONLY_GROUP> --exclude-newer <EXCLUDE_NEWER>
For more information, try '--help'.
"###);
Ok(())
}