Workaround for panic due to missing global validation in clap (#14368)

Clap does not perform global validation, so flag that are declared as
overriding can be set at the same time:
https://github.com/clap-rs/clap/issues/6049. This would previously cause
a panic. We work around this by choosing the yes-value always and
writing a warning.

An alternative would be erroring when both are set, but it's unclear to
me if this may break things we want to support. (`UV_OFFLINE=1 cargo run
-q pip --no-offline install tqdm --no-cache` is already banned).

Fixes https://github.com/astral-sh/uv/pull/14299

**Test Plan**

```
$ cargo run -q pip --offline install --no-offline tqdm --no-cache
  warning: Boolean flags on different levels are not correctly supported (https://github.com/clap-rs/clap/issues/6049)
    × No solution found when resolving dependencies:
    ╰─▶ Because tqdm was not found in the cache and you require tqdm, we can conclude that your requirements are unsatisfiable.

        hint: Packages were unavailable because the network was disabled. When the network is disabled, registry packages may only be read from the cache.
```
This commit is contained in:
konsti 2025-07-01 20:39:46 +02:00 committed by GitHub
parent 29fcd6faee
commit 06df95adbf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 170 additions and 97 deletions

View file

@ -1,7 +1,10 @@
use anstream::eprintln;
use uv_cache::Refresh; use uv_cache::Refresh;
use uv_configuration::ConfigSettings; use uv_configuration::ConfigSettings;
use uv_resolver::PrereleaseMode; use uv_resolver::PrereleaseMode;
use uv_settings::{Combine, PipOptions, ResolverInstallerOptions, ResolverOptions}; use uv_settings::{Combine, PipOptions, ResolverInstallerOptions, ResolverOptions};
use uv_warnings::owo_colors::OwoColorize;
use crate::{ use crate::{
BuildOptionsArgs, FetchArgs, IndexArgs, InstallerArgs, Maybe, RefreshArgs, ResolverArgs, BuildOptionsArgs, FetchArgs, IndexArgs, InstallerArgs, Maybe, RefreshArgs, ResolverArgs,
@ -9,12 +12,27 @@ use crate::{
}; };
/// Given a boolean flag pair (like `--upgrade` and `--no-upgrade`), resolve the value of the flag. /// Given a boolean flag pair (like `--upgrade` and `--no-upgrade`), resolve the value of the flag.
pub fn flag(yes: bool, no: bool) -> Option<bool> { pub fn flag(yes: bool, no: bool, name: &str) -> Option<bool> {
match (yes, no) { match (yes, no) {
(true, false) => Some(true), (true, false) => Some(true),
(false, true) => Some(false), (false, true) => Some(false),
(false, false) => None, (false, false) => None,
(..) => unreachable!("Clap should make this impossible"), (..) => {
eprintln!(
"{}{} `{}` and `{}` cannot be used together. \
Boolean flags on different levels are currently not supported \
(https://github.com/clap-rs/clap/issues/6049)",
"error".bold().red(),
":".bold(),
format!("--{name}").green(),
format!("--no-{name}").green(),
);
// No error forwarding since should eventually be solved on the clap side.
#[allow(clippy::exit)]
{
std::process::exit(2);
}
}
} }
} }
@ -26,7 +44,7 @@ impl From<RefreshArgs> for Refresh {
refresh_package, refresh_package,
} = value; } = value;
Self::from_args(flag(refresh, no_refresh), refresh_package) Self::from_args(flag(refresh, no_refresh, "no-refresh"), refresh_package)
} }
} }
@ -53,7 +71,7 @@ impl From<ResolverArgs> for PipOptions {
} = args; } = args;
Self { Self {
upgrade: flag(upgrade, no_upgrade), upgrade: flag(upgrade, no_upgrade, "no-upgrade"),
upgrade_package: Some(upgrade_package), upgrade_package: Some(upgrade_package),
index_strategy, index_strategy,
keyring_provider, keyring_provider,
@ -66,7 +84,7 @@ impl From<ResolverArgs> for PipOptions {
}, },
config_settings: config_setting config_settings: config_setting
.map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()), .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
no_build_isolation: flag(no_build_isolation, build_isolation), no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"),
no_build_isolation_package: Some(no_build_isolation_package), no_build_isolation_package: Some(no_build_isolation_package),
exclude_newer, exclude_newer,
link_mode, link_mode,
@ -96,16 +114,16 @@ impl From<InstallerArgs> for PipOptions {
} = args; } = args;
Self { Self {
reinstall: flag(reinstall, no_reinstall), reinstall: flag(reinstall, no_reinstall, "reinstall"),
reinstall_package: Some(reinstall_package), reinstall_package: Some(reinstall_package),
index_strategy, index_strategy,
keyring_provider, keyring_provider,
config_settings: config_setting config_settings: config_setting
.map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()), .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
no_build_isolation: flag(no_build_isolation, build_isolation), no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"),
exclude_newer, exclude_newer,
link_mode, link_mode,
compile_bytecode: flag(compile_bytecode, no_compile_bytecode), compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"),
no_sources: if no_sources { Some(true) } else { None }, no_sources: if no_sources { Some(true) } else { None },
..PipOptions::from(index_args) ..PipOptions::from(index_args)
} }
@ -140,9 +158,9 @@ impl From<ResolverInstallerArgs> for PipOptions {
} = args; } = args;
Self { Self {
upgrade: flag(upgrade, no_upgrade), upgrade: flag(upgrade, no_upgrade, "upgrade"),
upgrade_package: Some(upgrade_package), upgrade_package: Some(upgrade_package),
reinstall: flag(reinstall, no_reinstall), reinstall: flag(reinstall, no_reinstall, "reinstall"),
reinstall_package: Some(reinstall_package), reinstall_package: Some(reinstall_package),
index_strategy, index_strategy,
keyring_provider, keyring_provider,
@ -155,11 +173,11 @@ impl From<ResolverInstallerArgs> for PipOptions {
fork_strategy, fork_strategy,
config_settings: config_setting config_settings: config_setting
.map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()), .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
no_build_isolation: flag(no_build_isolation, build_isolation), no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"),
no_build_isolation_package: Some(no_build_isolation_package), no_build_isolation_package: Some(no_build_isolation_package),
exclude_newer, exclude_newer,
link_mode, link_mode,
compile_bytecode: flag(compile_bytecode, no_compile_bytecode), compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"),
no_sources: if no_sources { Some(true) } else { None }, no_sources: if no_sources { Some(true) } else { None },
..PipOptions::from(index_args) ..PipOptions::from(index_args)
} }
@ -289,7 +307,7 @@ pub fn resolver_options(
.filter_map(Maybe::into_option) .filter_map(Maybe::into_option)
.collect() .collect()
}), }),
upgrade: flag(upgrade, no_upgrade), upgrade: flag(upgrade, no_upgrade, "no-upgrade"),
upgrade_package: Some(upgrade_package), upgrade_package: Some(upgrade_package),
index_strategy, index_strategy,
keyring_provider, keyring_provider,
@ -303,13 +321,13 @@ pub fn resolver_options(
dependency_metadata: None, dependency_metadata: None,
config_settings: config_setting config_settings: config_setting
.map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()), .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
no_build_isolation: flag(no_build_isolation, build_isolation), no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"),
no_build_isolation_package: Some(no_build_isolation_package), no_build_isolation_package: Some(no_build_isolation_package),
exclude_newer, exclude_newer,
link_mode, link_mode,
no_build: flag(no_build, build), no_build: flag(no_build, build, "build"),
no_build_package: Some(no_build_package), no_build_package: Some(no_build_package),
no_binary: flag(no_binary, binary), no_binary: flag(no_binary, binary, "binary"),
no_binary_package: Some(no_binary_package), no_binary_package: Some(no_binary_package),
no_sources: if no_sources { Some(true) } else { None }, no_sources: if no_sources { Some(true) } else { None },
} }
@ -386,13 +404,13 @@ pub fn resolver_installer_options(
.filter_map(Maybe::into_option) .filter_map(Maybe::into_option)
.collect() .collect()
}), }),
upgrade: flag(upgrade, no_upgrade), upgrade: flag(upgrade, no_upgrade, "upgrade"),
upgrade_package: if upgrade_package.is_empty() { upgrade_package: if upgrade_package.is_empty() {
None None
} else { } else {
Some(upgrade_package) Some(upgrade_package)
}, },
reinstall: flag(reinstall, no_reinstall), reinstall: flag(reinstall, no_reinstall, "reinstall"),
reinstall_package: if reinstall_package.is_empty() { reinstall_package: if reinstall_package.is_empty() {
None None
} else { } else {
@ -410,7 +428,7 @@ pub fn resolver_installer_options(
dependency_metadata: None, dependency_metadata: None,
config_settings: config_setting config_settings: config_setting
.map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()), .map(|config_settings| config_settings.into_iter().collect::<ConfigSettings>()),
no_build_isolation: flag(no_build_isolation, build_isolation), no_build_isolation: flag(no_build_isolation, build_isolation, "build-isolation"),
no_build_isolation_package: if no_build_isolation_package.is_empty() { no_build_isolation_package: if no_build_isolation_package.is_empty() {
None None
} else { } else {
@ -418,14 +436,14 @@ pub fn resolver_installer_options(
}, },
exclude_newer, exclude_newer,
link_mode, link_mode,
compile_bytecode: flag(compile_bytecode, no_compile_bytecode), compile_bytecode: flag(compile_bytecode, no_compile_bytecode, "compile-bytecode"),
no_build: flag(no_build, build), no_build: flag(no_build, build, "build"),
no_build_package: if no_build_package.is_empty() { no_build_package: if no_build_package.is_empty() {
None None
} else { } else {
Some(no_build_package) Some(no_build_package)
}, },
no_binary: flag(no_binary, binary), no_binary: flag(no_binary, binary, "binary"),
no_binary_package: if no_binary_package.is_empty() { no_binary_package: if no_binary_package.is_empty() {
None None
} else { } else {

View file

@ -118,16 +118,20 @@ impl GlobalSettings {
}, },
show_settings: args.show_settings, show_settings: args.show_settings,
preview: PreviewMode::from( preview: PreviewMode::from(
flag(args.preview, args.no_preview) flag(args.preview, args.no_preview, "preview")
.combine(workspace.and_then(|workspace| workspace.globals.preview)) .combine(workspace.and_then(|workspace| workspace.globals.preview))
.unwrap_or(false), .unwrap_or(false),
), ),
python_preference, python_preference,
python_downloads: flag(args.allow_python_downloads, args.no_python_downloads) python_downloads: flag(
.map(PythonDownloads::from) args.allow_python_downloads,
.combine(env(env::UV_PYTHON_DOWNLOADS)) args.no_python_downloads,
.combine(workspace.and_then(|workspace| workspace.globals.python_downloads)) "python-downloads",
.unwrap_or_default(), )
.map(PythonDownloads::from)
.combine(env(env::UV_PYTHON_DOWNLOADS))
.combine(workspace.and_then(|workspace| workspace.globals.python_downloads))
.unwrap_or_default(),
// Disable the progress bar with `RUST_LOG` to avoid progress fragments interleaving // Disable the progress bar with `RUST_LOG` to avoid progress fragments interleaving
// with log messages. // with log messages.
no_progress: args.no_progress || std::env::var_os(EnvVars::RUST_LOG).is_some(), no_progress: args.no_progress || std::env::var_os(EnvVars::RUST_LOG).is_some(),
@ -161,7 +165,7 @@ pub(crate) struct NetworkSettings {
impl NetworkSettings { impl NetworkSettings {
pub(crate) fn resolve(args: &GlobalArgs, workspace: Option<&FilesystemOptions>) -> Self { pub(crate) fn resolve(args: &GlobalArgs, workspace: Option<&FilesystemOptions>) -> Self {
let connectivity = if flag(args.offline, args.no_offline) let connectivity = if flag(args.offline, args.no_offline, "offline")
.combine(workspace.and_then(|workspace| workspace.globals.offline)) .combine(workspace.and_then(|workspace| workspace.globals.offline))
.unwrap_or(false) .unwrap_or(false)
{ {
@ -169,7 +173,7 @@ impl NetworkSettings {
} else { } else {
Connectivity::Online Connectivity::Online
}; };
let native_tls = flag(args.native_tls, args.no_native_tls) let native_tls = flag(args.native_tls, args.no_native_tls, "native-tls")
.combine(workspace.and_then(|workspace| workspace.globals.native_tls)) .combine(workspace.and_then(|workspace| workspace.globals.native_tls))
.unwrap_or(false); .unwrap_or(false);
let allow_insecure_host = args let allow_insecure_host = args
@ -274,8 +278,12 @@ impl InitSettings {
(_, _, _) => unreachable!("`app`, `lib`, and `script` are mutually exclusive"), (_, _, _) => unreachable!("`app`, `lib`, and `script` are mutually exclusive"),
}; };
let package = flag(package || build_backend.is_some(), no_package || r#virtual) let package = flag(
.unwrap_or(kind.packaged_by_default()); package || build_backend.is_some(),
no_package || r#virtual,
"virtual",
)
.unwrap_or(kind.packaged_by_default());
let install_mirrors = filesystem let install_mirrors = filesystem
.map(|fs| fs.install_mirrors.clone()) .map(|fs| fs.install_mirrors.clone())
@ -295,7 +303,7 @@ impl InitSettings {
build_backend, build_backend,
no_readme: no_readme || bare, no_readme: no_readme || bare,
author_from, author_from,
pin_python: flag(pin_python, no_pin_python).unwrap_or(!bare), pin_python: flag(pin_python, no_pin_python, "pin-python").unwrap_or(!bare),
no_workspace, no_workspace,
python: python.and_then(Maybe::into_option), python: python.and_then(Maybe::into_option),
install_mirrors, install_mirrors,
@ -398,7 +406,7 @@ impl RunSettings {
false, false,
// TODO(blueraft): support only_extra // TODO(blueraft): support only_extra
vec![], vec![],
flag(all_extras, no_all_extras).unwrap_or_default(), flag(all_extras, no_all_extras, "all-extras").unwrap_or_default(),
), ),
groups: DependencyGroups::from_args( groups: DependencyGroups::from_args(
dev, dev,
@ -411,7 +419,7 @@ impl RunSettings {
all_groups, all_groups,
), ),
editable: EditableMode::from_args(no_editable), editable: EditableMode::from_args(no_editable),
modifications: if flag(exact, inexact).unwrap_or(false) { modifications: if flag(exact, inexact, "inexact").unwrap_or(false) {
Modifications::Exact Modifications::Exact
} else { } else {
Modifications::Sufficient Modifications::Sufficient
@ -434,7 +442,7 @@ impl RunSettings {
package, package,
no_project, no_project,
no_sync, no_sync,
active: flag(active, no_active), active: flag(active, no_active, "active"),
python: python.and_then(Maybe::into_option), python: python.and_then(Maybe::into_option),
refresh: Refresh::from(refresh), refresh: Refresh::from(refresh),
settings: ResolverInstallerSettings::combine( settings: ResolverInstallerSettings::combine(
@ -1081,7 +1089,7 @@ impl PythonFindSettings {
request, request,
show_version, show_version,
no_project, no_project,
system: flag(system, no_system).unwrap_or_default(), system: flag(system, no_system, "system").unwrap_or_default(),
} }
} }
} }
@ -1116,7 +1124,7 @@ impl PythonPinSettings {
Self { Self {
request, request,
resolved: flag(resolved, no_resolved).unwrap_or(false), resolved: flag(resolved, no_resolved, "resolved").unwrap_or(false),
no_project, no_project,
global, global,
rm, rm,
@ -1195,7 +1203,7 @@ impl SyncSettings {
filesystem, filesystem,
); );
let check = flag(check, no_check).unwrap_or_default(); let check = flag(check, no_check, "check").unwrap_or_default();
let dry_run = if check { let dry_run = if check {
DryRun::Check DryRun::Check
} else { } else {
@ -1207,7 +1215,7 @@ impl SyncSettings {
frozen, frozen,
dry_run, dry_run,
script, script,
active: flag(active, no_active), active: flag(active, no_active, "active"),
extras: ExtrasSpecification::from_args( extras: ExtrasSpecification::from_args(
extra.unwrap_or_default(), extra.unwrap_or_default(),
no_extra, no_extra,
@ -1215,7 +1223,7 @@ impl SyncSettings {
false, false,
// TODO(blueraft): support only_extra // TODO(blueraft): support only_extra
vec![], vec![],
flag(all_extras, no_all_extras).unwrap_or_default(), flag(all_extras, no_all_extras, "all-extras").unwrap_or_default(),
), ),
groups: DependencyGroups::from_args( groups: DependencyGroups::from_args(
dev, dev,
@ -1233,7 +1241,7 @@ impl SyncSettings {
no_install_workspace, no_install_workspace,
no_install_package, no_install_package,
), ),
modifications: if flag(exact, inexact).unwrap_or(true) { modifications: if flag(exact, inexact, "inexact").unwrap_or(true) {
Modifications::Exact Modifications::Exact
} else { } else {
Modifications::Sufficient Modifications::Sufficient
@ -1437,7 +1445,7 @@ impl AddSettings {
Self { Self {
locked, locked,
frozen, frozen,
active: flag(active, no_active), active: flag(active, no_active, "active"),
no_sync, no_sync,
packages, packages,
requirements, requirements,
@ -1455,7 +1463,7 @@ impl AddSettings {
package, package,
script, script,
python: python.and_then(Maybe::into_option), python: python.and_then(Maybe::into_option),
editable: flag(editable, no_editable), editable: flag(editable, no_editable, "editable"),
extras: extra.unwrap_or_default(), extras: extra.unwrap_or_default(),
refresh: Refresh::from(refresh), refresh: Refresh::from(refresh),
indexes, indexes,
@ -1531,7 +1539,7 @@ impl RemoveSettings {
Self { Self {
locked, locked,
frozen, frozen,
active: flag(active, no_active), active: flag(active, no_active, "active"),
no_sync, no_sync,
packages, packages,
dependency_type, dependency_type,
@ -1603,7 +1611,7 @@ impl VersionSettings {
dry_run, dry_run,
locked, locked,
frozen, frozen,
active: flag(active, no_active), active: flag(active, no_active, "active"),
no_sync, no_sync,
package, package,
python: python.and_then(Maybe::into_option), python: python.and_then(Maybe::into_option),
@ -1779,7 +1787,7 @@ impl ExportSettings {
false, false,
// TODO(blueraft): support only_extra // TODO(blueraft): support only_extra
vec![], vec![],
flag(all_extras, no_all_extras).unwrap_or_default(), flag(all_extras, no_all_extras, "all-extras").unwrap_or_default(),
), ),
groups: DependencyGroups::from_args( groups: DependencyGroups::from_args(
dev, dev,
@ -1792,7 +1800,7 @@ impl ExportSettings {
all_groups, all_groups,
), ),
editable: EditableMode::from_args(no_editable), editable: EditableMode::from_args(no_editable),
hashes: flag(hashes, no_hashes).unwrap_or(true), hashes: flag(hashes, no_hashes, "hashes").unwrap_or(true),
install_options: InstallOptions::new( install_options: InstallOptions::new(
no_emit_project, no_emit_project,
no_emit_workspace, no_emit_workspace,
@ -1801,8 +1809,8 @@ impl ExportSettings {
output_file, output_file,
locked, locked,
frozen, frozen,
include_annotations: flag(annotate, no_annotate).unwrap_or(true), include_annotations: flag(annotate, no_annotate, "annotate").unwrap_or(true),
include_header: flag(header, no_header).unwrap_or(true), include_header: flag(header, no_header, "header").unwrap_or(true),
script, script,
python: python.and_then(Maybe::into_option), python: python.and_then(Maybe::into_option),
refresh: Refresh::from(refresh), refresh: Refresh::from(refresh),
@ -1955,30 +1963,42 @@ impl PipCompileSettings {
settings: PipSettings::combine( settings: PipSettings::combine(
PipOptions { PipOptions {
python: python.and_then(Maybe::into_option), python: python.and_then(Maybe::into_option),
system: flag(system, no_system), system: flag(system, no_system, "system"),
no_build: flag(no_build, build), no_build: flag(no_build, build, "build"),
no_binary, no_binary,
only_binary, only_binary,
extra, extra,
all_extras: flag(all_extras, no_all_extras), all_extras: flag(all_extras, no_all_extras, "all-extras"),
no_deps: flag(no_deps, deps), no_deps: flag(no_deps, deps, "deps"),
group: Some(group), group: Some(group),
output_file, output_file,
no_strip_extras: flag(no_strip_extras, strip_extras), no_strip_extras: flag(no_strip_extras, strip_extras, "strip-extras"),
no_strip_markers: flag(no_strip_markers, strip_markers), no_strip_markers: flag(no_strip_markers, strip_markers, "strip-markers"),
no_annotate: flag(no_annotate, annotate), no_annotate: flag(no_annotate, annotate, "annotate"),
no_header: flag(no_header, header), no_header: flag(no_header, header, "header"),
custom_compile_command, custom_compile_command,
generate_hashes: flag(generate_hashes, no_generate_hashes), generate_hashes: flag(generate_hashes, no_generate_hashes, "generate-hashes"),
python_version, python_version,
python_platform, python_platform,
universal: flag(universal, no_universal), universal: flag(universal, no_universal, "universal"),
no_emit_package, no_emit_package,
emit_index_url: flag(emit_index_url, no_emit_index_url), emit_index_url: flag(emit_index_url, no_emit_index_url, "emit-index-url"),
emit_find_links: flag(emit_find_links, no_emit_find_links), emit_find_links: flag(emit_find_links, no_emit_find_links, "emit-find-links"),
emit_build_options: flag(emit_build_options, no_emit_build_options), emit_build_options: flag(
emit_marker_expression: flag(emit_marker_expression, no_emit_marker_expression), emit_build_options,
emit_index_annotation: flag(emit_index_annotation, no_emit_index_annotation), no_emit_build_options,
"emit-build-options",
),
emit_marker_expression: flag(
emit_marker_expression,
no_emit_marker_expression,
"emit-marker-expression",
),
emit_index_annotation: flag(
emit_index_annotation,
no_emit_index_annotation,
"emit-index-annotation",
),
annotation_style, annotation_style,
torch_backend, torch_backend,
..PipOptions::from(resolver) ..PipOptions::from(resolver)
@ -2050,22 +2070,27 @@ impl PipSyncSettings {
settings: PipSettings::combine( settings: PipSettings::combine(
PipOptions { PipOptions {
python: python.and_then(Maybe::into_option), python: python.and_then(Maybe::into_option),
system: flag(system, no_system), system: flag(system, no_system, "system"),
break_system_packages: flag(break_system_packages, no_break_system_packages), break_system_packages: flag(
break_system_packages,
no_break_system_packages,
"break-system-packages",
),
target, target,
prefix, prefix,
require_hashes: flag(require_hashes, no_require_hashes), require_hashes: flag(require_hashes, no_require_hashes, "require-hashes"),
verify_hashes: flag(verify_hashes, no_verify_hashes), verify_hashes: flag(verify_hashes, no_verify_hashes, "verify-hashes"),
no_build: flag(no_build, build), no_build: flag(no_build, build, "build"),
no_binary, no_binary,
only_binary, only_binary,
allow_empty_requirements: flag( allow_empty_requirements: flag(
allow_empty_requirements, allow_empty_requirements,
no_allow_empty_requirements, no_allow_empty_requirements,
"allow-empty-requirements",
), ),
python_version, python_version,
python_platform, python_platform,
strict: flag(strict, no_strict), strict: flag(strict, no_strict, "strict"),
torch_backend, torch_backend,
..PipOptions::from(installer) ..PipOptions::from(installer)
}, },
@ -2199,7 +2224,7 @@ impl PipInstallSettings {
constraints_from_workspace, constraints_from_workspace,
overrides_from_workspace, overrides_from_workspace,
build_constraints_from_workspace, build_constraints_from_workspace,
modifications: if flag(exact, inexact).unwrap_or(false) { modifications: if flag(exact, inexact, "inexact").unwrap_or(false) {
Modifications::Exact Modifications::Exact
} else { } else {
Modifications::Sufficient Modifications::Sufficient
@ -2208,22 +2233,26 @@ impl PipInstallSettings {
settings: PipSettings::combine( settings: PipSettings::combine(
PipOptions { PipOptions {
python: python.and_then(Maybe::into_option), python: python.and_then(Maybe::into_option),
system: flag(system, no_system), system: flag(system, no_system, "system"),
break_system_packages: flag(break_system_packages, no_break_system_packages), break_system_packages: flag(
break_system_packages,
no_break_system_packages,
"break-system-packages",
),
target, target,
prefix, prefix,
no_build: flag(no_build, build), no_build: flag(no_build, build, "build"),
no_binary, no_binary,
only_binary, only_binary,
strict: flag(strict, no_strict), strict: flag(strict, no_strict, "strict"),
extra, extra,
all_extras: flag(all_extras, no_all_extras), all_extras: flag(all_extras, no_all_extras, "all-extras"),
group: Some(group), group: Some(group),
no_deps: flag(no_deps, deps), no_deps: flag(no_deps, deps, "deps"),
python_version, python_version,
python_platform, python_platform,
require_hashes: flag(require_hashes, no_require_hashes), require_hashes: flag(require_hashes, no_require_hashes, "require-hashes"),
verify_hashes: flag(verify_hashes, no_verify_hashes), verify_hashes: flag(verify_hashes, no_verify_hashes, "verify-hashes"),
torch_backend, torch_backend,
..PipOptions::from(installer) ..PipOptions::from(installer)
}, },
@ -2267,8 +2296,12 @@ impl PipUninstallSettings {
settings: PipSettings::combine( settings: PipSettings::combine(
PipOptions { PipOptions {
python: python.and_then(Maybe::into_option), python: python.and_then(Maybe::into_option),
system: flag(system, no_system), system: flag(system, no_system, "system"),
break_system_packages: flag(break_system_packages, no_break_system_packages), break_system_packages: flag(
break_system_packages,
no_break_system_packages,
"break-system-packages",
),
target, target,
prefix, prefix,
keyring_provider, keyring_provider,
@ -2308,8 +2341,8 @@ impl PipFreezeSettings {
settings: PipSettings::combine( settings: PipSettings::combine(
PipOptions { PipOptions {
python: python.and_then(Maybe::into_option), python: python.and_then(Maybe::into_option),
system: flag(system, no_system), system: flag(system, no_system, "system"),
strict: flag(strict, no_strict), strict: flag(strict, no_strict, "strict"),
..PipOptions::default() ..PipOptions::default()
}, },
filesystem, filesystem,
@ -2348,15 +2381,15 @@ impl PipListSettings {
} = args; } = args;
Self { Self {
editable: flag(editable, exclude_editable), editable: flag(editable, exclude_editable, "exclude-editable"),
exclude, exclude,
format, format,
outdated: flag(outdated, no_outdated).unwrap_or(false), outdated: flag(outdated, no_outdated, "outdated").unwrap_or(false),
settings: PipSettings::combine( settings: PipSettings::combine(
PipOptions { PipOptions {
python: python.and_then(Maybe::into_option), python: python.and_then(Maybe::into_option),
system: flag(system, no_system), system: flag(system, no_system, "system"),
strict: flag(strict, no_strict), strict: flag(strict, no_strict, "strict"),
..PipOptions::from(fetch) ..PipOptions::from(fetch)
}, },
filesystem, filesystem,
@ -2393,8 +2426,8 @@ impl PipShowSettings {
settings: PipSettings::combine( settings: PipSettings::combine(
PipOptions { PipOptions {
python: python.and_then(Maybe::into_option), python: python.and_then(Maybe::into_option),
system: flag(system, no_system), system: flag(system, no_system, "system"),
strict: flag(strict, no_strict), strict: flag(strict, no_strict, "strict"),
..PipOptions::default() ..PipOptions::default()
}, },
filesystem, filesystem,
@ -2442,8 +2475,8 @@ impl PipTreeSettings {
settings: PipSettings::combine( settings: PipSettings::combine(
PipOptions { PipOptions {
python: python.and_then(Maybe::into_option), python: python.and_then(Maybe::into_option),
system: flag(system, no_system), system: flag(system, no_system, "system"),
strict: flag(strict, no_strict), strict: flag(strict, no_strict, "strict"),
..PipOptions::from(fetch) ..PipOptions::from(fetch)
}, },
filesystem, filesystem,
@ -2471,7 +2504,7 @@ impl PipCheckSettings {
settings: PipSettings::combine( settings: PipSettings::combine(
PipOptions { PipOptions {
python: python.and_then(Maybe::into_option), python: python.and_then(Maybe::into_option),
system: flag(system, no_system), system: flag(system, no_system, "system"),
..PipOptions::default() ..PipOptions::default()
}, },
filesystem, filesystem,
@ -2538,15 +2571,15 @@ impl BuildSettings {
sdist, sdist,
wheel, wheel,
list, list,
build_logs: flag(build_logs, no_build_logs).unwrap_or(true), build_logs: flag(build_logs, no_build_logs, "build-logs").unwrap_or(true),
build_constraints: build_constraints build_constraints: build_constraints
.into_iter() .into_iter()
.filter_map(Maybe::into_option) .filter_map(Maybe::into_option)
.collect(), .collect(),
force_pep517, force_pep517,
hash_checking: HashCheckingMode::from_args( hash_checking: HashCheckingMode::from_args(
flag(require_hashes, no_require_hashes), flag(require_hashes, no_require_hashes, "require-hashes"),
flag(verify_hashes, no_verify_hashes), flag(verify_hashes, no_verify_hashes, "verify-hashes"),
), ),
python: python.and_then(Maybe::into_option), python: python.and_then(Maybe::into_option),
refresh: Refresh::from(refresh), refresh: Refresh::from(refresh),
@ -2605,7 +2638,7 @@ impl VenvSettings {
settings: PipSettings::combine( settings: PipSettings::combine(
PipOptions { PipOptions {
python: python.and_then(Maybe::into_option), python: python.and_then(Maybe::into_option),
system: flag(system, no_system), system: flag(system, no_system, "system"),
index_strategy, index_strategy,
keyring_provider, keyring_provider,
exclude_newer, exclude_newer,

View file

@ -11486,3 +11486,25 @@ fn pep_751_dependency() -> Result<()> {
Ok(()) Ok(())
} }
/// Test that we show an error instead of panicking for conflicting arguments in different levels,
/// which are not caught by clap.
#[test]
fn conflicting_flags_clap_bug() {
let context = TestContext::new("3.12");
uv_snapshot!(context.filters(), context.command()
.arg("pip")
.arg("--offline")
.arg("install")
.arg("--no-offline")
.arg("tqdm"), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: `--offline` and `--no-offline` cannot be used together. Boolean flags on different levels are currently not supported (https://github.com/clap-rs/clap/issues/6049)
"
);
}