Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions clippy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@ doc-valid-idents = [
"ROCm",
"XPU",
"PowerShell",
"UV_DEV",
"UV_FROZEN",
"UV_ISOLATED",
"UV_LOCKED",
"UV_MANAGED_PYTHON",
"UV_NATIVE_TLS",
"UV_NO_DEV",
"UV_NO_EDITABLE",
"UV_NO_ENV_FILE",
"UV_NO_INSTALLER_METADATA",
"UV_NO_MANAGED_PYTHON",
"UV_NO_PROGRESS",
"UV_NO_SYNC",
"UV_OFFLINE",
"UV_PREVIEW",
"UV_SHOW_RESOLUTION",
"UV_VENV_CLEAR",
"UV_VENV_SEED",
".." # Include the defaults
]

Expand Down
223 changes: 108 additions & 115 deletions crates/uv-cli/src/lib.rs

Large diffs are not rendered by default.

148 changes: 147 additions & 1 deletion crates/uv-cli/src/options.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use std::fmt;

use anstream::eprintln;

use uv_cache::Refresh;
use uv_configuration::{BuildIsolation, Reinstall, Upgrade};
use uv_distribution_types::{ConfigSettings, PackageConfigSettings, Requirement};
use uv_resolver::{ExcludeNewer, ExcludeNewerPackage, PrereleaseMode};
use uv_settings::{Combine, PipOptions, ResolverInstallerOptions, ResolverOptions};
use uv_settings::{Combine, EnvFlag, PipOptions, ResolverInstallerOptions, ResolverOptions};
use uv_warnings::owo_colors::OwoColorize;

use crate::{
Expand Down Expand Up @@ -37,6 +39,150 @@ pub fn flag(yes: bool, no: bool, name: &str) -> Option<bool> {
}
}

/// The source of a boolean flag value.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FlagSource {
/// The flag was set via command-line argument.
Cli,
/// The flag was set via environment variable.
Env(&'static str),
/// The flag was set via workspace/project configuration.
Config,
}

impl fmt::Display for FlagSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Cli => write!(f, "command-line argument"),
Self::Env(name) => write!(f, "environment variable `{name}`"),
Self::Config => write!(f, "workspace configuration"),
}
}
}

/// A boolean flag value with its source.
#[derive(Debug, Clone, Copy)]
pub enum Flag {
/// The flag is not set.
Disabled,
/// The flag is enabled with a known source.
Enabled {
source: FlagSource,
/// The CLI flag name (e.g., "locked" for `--locked`).
name: &'static str,
},
}

impl Flag {
/// Create a flag that is explicitly disabled.
pub const fn disabled() -> Self {
Self::Disabled
}

/// Create an enabled flag from a CLI argument.
pub const fn from_cli(name: &'static str) -> Self {
Self::Enabled {
source: FlagSource::Cli,
name,
}
}

/// Create an enabled flag from workspace/project configuration.
pub const fn from_config(name: &'static str) -> Self {
Self::Enabled {
source: FlagSource::Config,
name,
}
}

/// Returns `true` if the flag is set.
pub fn is_enabled(self) -> bool {
matches!(self, Self::Enabled { .. })
}

/// Returns the source of the flag, if it is set.
pub fn source(self) -> Option<FlagSource> {
match self {
Self::Disabled => None,
Self::Enabled { source, .. } => Some(source),
}
}

/// Returns the CLI flag name, if the flag is enabled.
pub fn name(self) -> Option<&'static str> {
match self {
Self::Disabled => None,
Self::Enabled { name, .. } => Some(name),
}
}
}

impl From<Flag> for bool {
fn from(flag: Flag) -> Self {
flag.is_enabled()
}
}

/// Resolve a boolean flag from CLI arguments and an environment variable.
///
/// The CLI argument takes precedence over the environment variable. Returns a [`Flag`] with the
/// resolved value and source.
pub fn resolve_flag(cli_flag: bool, name: &'static str, env_flag: EnvFlag) -> Flag {
if cli_flag {
Flag::Enabled {
source: FlagSource::Cli,
name,
}
} else if env_flag.value == Some(true) {
Flag::Enabled {
source: FlagSource::Env(env_flag.env_var),
name,
}
} else {
Flag::Disabled
}
}

/// Check if two flags conflict and exit with an error if they do.
///
/// This function checks if both flags are enabled (truthy) and reports an error if so, including
/// the source of each flag (CLI or environment variable) in the error message.
pub fn check_conflicts(flag_a: Flag, flag_b: Flag) {
if let (
Flag::Enabled {
source: source_a,
name: name_a,
},
Flag::Enabled {
source: source_b,
name: name_b,
},
) = (flag_a, flag_b)
{
let display_a = match source_a {
FlagSource::Cli => format!("`--{name_a}`"),
FlagSource::Env(env) => format!("`{env}` (environment variable)"),
FlagSource::Config => format!("`{name_a}` (workspace configuration)"),
};
let display_b = match source_b {
FlagSource::Cli => format!("`--{name_b}`"),
FlagSource::Env(env) => format!("`{env}` (environment variable)"),
FlagSource::Config => format!("`{name_b}` (workspace configuration)"),
};
eprintln!(
"{}{} the argument {} cannot be used with {}",
"error".bold().red(),
":".bold(),
display_a.green(),
display_b.green(),
);
#[allow(clippy::exit)]
{
std::process::exit(2);
}
}
}

impl From<RefreshArgs> for Refresh {
fn from(value: RefreshArgs) -> Self {
let RefreshArgs {
Expand Down
55 changes: 55 additions & 0 deletions crates/uv-settings/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,25 @@ pub struct Concurrency {
pub installs: Option<NonZeroUsize>,
}

/// A boolean flag parsed from an environment variable.
///
/// Stores both the value and the environment variable name for use in error messages.
#[derive(Debug, Clone, Copy)]
pub struct EnvFlag {
pub value: Option<bool>,
pub env_var: &'static str,
}

impl EnvFlag {
/// Create a new [`EnvFlag`] by parsing the given environment variable.
pub fn new(env_var: &'static str) -> Result<Self, Error> {
Ok(Self {
value: parse_boolish_environment_variable(env_var)?,
env_var,
})
}
}

/// Options loaded from environment variables.
///
/// This is currently a subset of all respected environment variables, most are parsed via Clap at
Expand All @@ -601,6 +620,24 @@ pub struct EnvironmentOptions {
pub concurrency: Concurrency,
#[cfg(feature = "tracing-durations-export")]
pub tracing_durations_file: Option<PathBuf>,
pub frozen: EnvFlag,
pub locked: EnvFlag,
pub offline: EnvFlag,
pub no_sync: EnvFlag,
pub managed_python: EnvFlag,
pub no_managed_python: EnvFlag,
pub native_tls: EnvFlag,
pub preview: EnvFlag,
pub isolated: EnvFlag,
pub no_progress: EnvFlag,
pub no_installer_metadata: EnvFlag,
pub dev: EnvFlag,
pub no_dev: EnvFlag,
pub show_resolution: EnvFlag,
pub no_editable: EnvFlag,
pub no_env_file: EnvFlag,
pub venv_seed: EnvFlag,
pub venv_clear: EnvFlag,
}

impl EnvironmentOptions {
Expand Down Expand Up @@ -655,6 +692,24 @@ impl EnvironmentOptions {
tracing_durations_file: parse_path_environment_variable(
EnvVars::TRACING_DURATIONS_FILE,
),
frozen: EnvFlag::new(EnvVars::UV_FROZEN)?,
locked: EnvFlag::new(EnvVars::UV_LOCKED)?,
offline: EnvFlag::new(EnvVars::UV_OFFLINE)?,
no_sync: EnvFlag::new(EnvVars::UV_NO_SYNC)?,
managed_python: EnvFlag::new(EnvVars::UV_MANAGED_PYTHON)?,
no_managed_python: EnvFlag::new(EnvVars::UV_NO_MANAGED_PYTHON)?,
native_tls: EnvFlag::new(EnvVars::UV_NATIVE_TLS)?,
preview: EnvFlag::new(EnvVars::UV_PREVIEW)?,
isolated: EnvFlag::new(EnvVars::UV_ISOLATED)?,
no_progress: EnvFlag::new(EnvVars::UV_NO_PROGRESS)?,
no_installer_metadata: EnvFlag::new(EnvVars::UV_NO_INSTALLER_METADATA)?,
dev: EnvFlag::new(EnvVars::UV_DEV)?,
no_dev: EnvFlag::new(EnvVars::UV_NO_DEV)?,
show_resolution: EnvFlag::new(EnvVars::UV_SHOW_RESOLUTION)?,
no_editable: EnvFlag::new(EnvVars::UV_NO_EDITABLE)?,
no_env_file: EnvFlag::new(EnvVars::UV_NO_ENV_FILE)?,
venv_seed: EnvFlag::new(EnvVars::UV_VENV_SEED)?,
venv_clear: EnvFlag::new(EnvVars::UV_VENV_CLEAR)?,
})
}
}
Expand Down
Loading
Loading