diff --git a/clippy.toml b/clippy.toml index 82c596a41a30a..bbb97e8a1044b 100644 --- a/clippy.toml +++ b/clippy.toml @@ -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 ] diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 28398df263223..e89d8838a0294 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -165,33 +165,27 @@ pub struct GlobalArgs { )] pub python_preference: Option, - /// Require use of uv-managed Python versions. + /// Require use of uv-managed Python versions [env: UV_MANAGED_PYTHON=] /// - /// By default, uv prefers using Python versions it manages. However, it - /// will use system Python versions if a uv-managed Python is not - /// installed. This option disables use of system Python versions. + /// By default, uv prefers using Python versions it manages. However, it will use system Python + /// versions if a uv-managed Python is not installed. This option disables use of system Python + /// versions. #[arg( global = true, long, help_heading = "Python options", - env = EnvVars::UV_MANAGED_PYTHON, - value_parser = clap::builder::BoolishValueParser::new(), - overrides_with = "no_managed_python", - conflicts_with = "python_preference" + overrides_with = "no_managed_python" )] pub managed_python: bool, - /// Disable use of uv-managed Python versions. + /// Disable use of uv-managed Python versions [env: UV_NO_MANAGED_PYTHON=] /// /// Instead, uv will search for a suitable Python version on the system. #[arg( global = true, long, help_heading = "Python options", - env = EnvVars::UV_NO_MANAGED_PYTHON, - value_parser = clap::builder::BoolishValueParser::new(), - overrides_with = "managed_python", - conflicts_with = "python_preference" + overrides_with = "managed_python" )] pub no_managed_python: bool, @@ -241,7 +235,7 @@ pub struct GlobalArgs { )] pub color: Option, - /// Whether to load TLS certificates from the platform's native certificate store. + /// Whether to load TLS certificates from the platform's native store [env: UV_NATIVE_TLS=] /// /// By default, uv loads certificates from the bundled `webpki-roots` crate. The /// `webpki-roots` are a reliable set of trust roots from Mozilla, and including them in uv @@ -250,16 +244,16 @@ pub struct GlobalArgs { /// However, in some cases, you may want to use the platform's native certificate store, /// especially if you're relying on a corporate trust root (e.g., for a mandatory proxy) that's /// included in your system's certificate store. - #[arg(global = true, long, env = EnvVars::UV_NATIVE_TLS, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_native_tls"))] + #[arg(global = true, long, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_native_tls"))] pub native_tls: bool, #[arg(global = true, long, overrides_with("native_tls"), hide = true)] pub no_native_tls: bool, - /// Disable network access. + /// Disable network access [env: UV_OFFLINE=] /// /// When disabled, uv will only use locally cached data and locally available files. - #[arg(global = true, long, overrides_with("no_offline"), env = EnvVars::UV_OFFLINE, value_parser = clap::builder::BoolishValueParser::new())] + #[arg(global = true, long, overrides_with("no_offline"))] pub offline: bool, #[arg(global = true, long, overrides_with("offline"), hide = true)] @@ -286,10 +280,10 @@ pub struct GlobalArgs { )] pub allow_insecure_host: Option>>, - /// Whether to enable all experimental preview features. + /// Whether to enable all experimental preview features [env: UV_PREVIEW=] /// /// Preview features may change without warning. - #[arg(global = true, long, hide = true, env = EnvVars::UV_PREVIEW, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_preview"))] + #[arg(global = true, long, hide = true, value_parser = clap::builder::BoolishValueParser::new(), overrides_with("no_preview"))] pub preview: bool, #[arg(global = true, long, overrides_with("preview"), hide = true)] @@ -314,13 +308,13 @@ pub struct GlobalArgs { )] pub preview_features: Vec, - /// Avoid discovering a `pyproject.toml` or `uv.toml` file. + /// Avoid discovering a `pyproject.toml` or `uv.toml` file [env: UV_ISOLATED=] /// /// Normally, configuration files are discovered in the current directory, /// parent directories, or user configuration directories. /// /// This option is deprecated in favor of `--no-config`. - #[arg(global = true, long, hide = true, env = EnvVars::UV_ISOLATED, value_parser = clap::builder::BoolishValueParser::new())] + #[arg(global = true, long, hide = true, value_parser = clap::builder::BoolishValueParser::new())] pub isolated: bool, /// Show the resolved settings for the current command. @@ -329,14 +323,15 @@ pub struct GlobalArgs { #[arg(global = true, long, hide = true)] pub show_settings: bool, - /// Hide all progress outputs. + /// Hide all progress outputs [env: UV_NO_PROGRESS=] /// /// For example, spinners or progress bars. - #[arg(global = true, long, env = EnvVars::UV_NO_PROGRESS, value_parser = clap::builder::BoolishValueParser::new())] + #[arg(global = true, long, value_parser = clap::builder::BoolishValueParser::new())] pub no_progress: bool, - /// Skip writing `uv` installer metadata files (e.g., `INSTALLER`, `REQUESTED`, and `direct_url.json`) to site-packages `.dist-info` directories. - #[arg(global = true, long, hide = true, env = EnvVars::UV_NO_INSTALLER_METADATA, value_parser = clap::builder::BoolishValueParser::new())] + /// Skip writing `uv` installer metadata files (e.g., `INSTALLER`, `REQUESTED`, and + /// `direct_url.json`) to site-packages `.dist-info` directories [env: UV_NO_INSTALLER_METADATA=] + #[arg(global = true, long, hide = true, value_parser = clap::builder::BoolishValueParser::new())] pub no_installer_metadata: bool, /// Change to the given directory prior to running the command. @@ -612,8 +607,8 @@ pub struct VersionArgs { #[arg(long, value_enum, default_value = "text")] pub output_format: VersionFormat, - /// Avoid syncing the virtual environment after re-locking the project. - #[arg(long, env = EnvVars::UV_NO_SYNC, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with = "frozen")] + /// Avoid syncing the virtual environment after re-locking the project [env: UV_NO_SYNC=] + #[arg(long)] pub no_sync: bool, /// Prefer the active virtual environment over the project's virtual environment. @@ -629,17 +624,17 @@ pub struct VersionArgs { #[arg(long, overrides_with = "active", hide = true)] pub no_active: bool, - /// Assert that the `uv.lock` will remain unchanged. + /// Assert that the `uv.lock` will remain unchanged [env: UV_LOCKED=] /// /// Requires that the lockfile is up-to-date. If the lockfile is missing or needs to be updated, /// uv will exit with an error. - #[arg(long, env = EnvVars::UV_LOCKED, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["frozen", "upgrade"])] + #[arg(long, conflicts_with_all = ["frozen", "upgrade"])] pub locked: bool, - /// Update the version without re-locking the project. + /// Update the version without re-locking the project [env: UV_FROZEN=] /// /// The project environment will not be synced. - #[arg(long, env = EnvVars::UV_FROZEN, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["locked", "upgrade", "no_sources"])] + #[arg(long, conflicts_with_all = ["locked", "upgrade", "no_sources"])] pub frozen: bool, #[command(flatten)] @@ -3088,18 +3083,19 @@ pub struct VenvArgs { #[arg(long, alias = "no-workspace")] pub no_project: bool, - /// Install seed packages (one or more of: `pip`, `setuptools`, and `wheel`) into the virtual environment. + /// Install seed packages (one or more of: `pip`, `setuptools`, and `wheel`) into the virtual + /// environment [env: UV_VENV_SEED=] /// /// Note that `setuptools` and `wheel` are not included in Python 3.12+ environments. - #[arg(long, value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_VENV_SEED)] + #[arg(long, value_parser = clap::builder::BoolishValueParser::new())] pub seed: bool, - /// Remove any existing files or directories at the target path. + /// Remove any existing files or directories at the target path [env: UV_VENV_CLEAR=] /// /// By default, `uv venv` will exit with an error if the given path is non-empty. The /// `--clear` option will instead clear a non-empty path before creating a new virtual /// environment. - #[clap(long, short, overrides_with = "allow_existing", value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_VENV_CLEAR)] + #[clap(long, short, overrides_with = "allow_existing", value_parser = clap::builder::BoolishValueParser::new())] pub clear: bool, /// Fail without prompting if any existing files or directories are present at the target path. @@ -3481,7 +3477,7 @@ pub struct RunArgs { #[arg(long, overrides_with("all_extras"), hide = true)] pub no_all_extras: bool, - /// Include the development dependency group. + /// Include the development dependency group [env: UV_DEV=] /// /// Development dependencies are defined via `dependency-groups.dev` or /// `tool.uv.dev-dependencies` in a `pyproject.toml`. @@ -3489,16 +3485,16 @@ pub struct RunArgs { /// This option is an alias for `--group dev`. /// /// This option is only available when running in a project. - #[arg(long, overrides_with("no_dev"), hide = true, env = EnvVars::UV_DEV, value_parser = clap::builder::BoolishValueParser::new())] + #[arg(long, overrides_with("no_dev"), hide = true, value_parser = clap::builder::BoolishValueParser::new())] pub dev: bool, - /// Disable the development dependency group. + /// Disable the development dependency group [env: UV_NO_DEV=] /// /// This option is an alias of `--no-group dev`. /// See `--no-default-groups` to disable all default groups instead. /// /// This option is only available when running in a project. - #[arg(long, overrides_with("dev"), env = EnvVars::UV_NO_DEV, value_parser = clap::builder::BoolishValueParser::new())] + #[arg(long, overrides_with("dev"), value_parser = clap::builder::BoolishValueParser::new())] pub no_dev: bool, /// Include dependencies from the specified dependency group. @@ -3557,8 +3553,8 @@ pub struct RunArgs { pub editable: bool, /// Install any editable dependencies, including the project and any workspace members, as - /// non-editable. - #[arg(long, overrides_with = "editable", value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_NO_EDITABLE)] + /// non-editable [env: UV_NO_EDITABLE=] + #[arg(long, overrides_with = "editable", value_parser = clap::builder::BoolishValueParser::new())] pub no_editable: bool, /// Do not remove extraneous packages present in the environment. @@ -3579,8 +3575,8 @@ pub struct RunArgs { #[arg(long, env = EnvVars::UV_ENV_FILE, value_hint = ValueHint::FilePath)] pub env_file: Vec, - /// Avoid reading environment variables from a `.env` file. - #[arg(long, value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_NO_ENV_FILE)] + /// Avoid reading environment variables from a `.env` file [env: UV_NO_ENV_FILE=] + #[arg(long, value_parser = clap::builder::BoolishValueParser::new())] pub no_env_file: bool, /// The command to run. @@ -3617,7 +3613,7 @@ pub struct RunArgs { #[arg(long, value_delimiter = ',', value_parser = parse_maybe_file_path, value_hint = ValueHint::FilePath)] pub with_requirements: Vec>, - /// Run the command in an isolated virtual environment. + /// Run the command in an isolated virtual environment [env: UV_ISOLATED=] /// /// Usually, the project environment is reused for performance. This option forces a fresh /// environment to be used for the project, enforcing strict isolation between dependencies and @@ -3627,7 +3623,7 @@ pub struct RunArgs { /// /// When used with `--with` or `--with-requirements`, the additional dependencies will still be /// layered in a second environment. - #[arg(long, env = EnvVars::UV_ISOLATED, value_parser = clap::builder::BoolishValueParser::new())] + #[arg(long, value_parser = clap::builder::BoolishValueParser::new())] pub isolated: bool, /// Prefer the active virtual environment over the project's virtual environment. @@ -3643,27 +3639,27 @@ pub struct RunArgs { #[arg(long, overrides_with = "active", hide = true)] pub no_active: bool, - /// Avoid syncing the virtual environment. + /// Avoid syncing the virtual environment [env: UV_NO_SYNC=] /// /// Implies `--frozen`, as the project dependencies will be ignored (i.e., the lockfile will not /// be updated, since the environment will not be synced regardless). - #[arg(long, env = EnvVars::UV_NO_SYNC, value_parser = clap::builder::BoolishValueParser::new())] + #[arg(long, value_parser = clap::builder::BoolishValueParser::new())] pub no_sync: bool, - /// Assert that the `uv.lock` will remain unchanged. + /// Assert that the `uv.lock` will remain unchanged [env: UV_LOCKED=] /// /// Requires that the lockfile is up-to-date. If the lockfile is missing or /// needs to be updated, uv will exit with an error. - #[arg(long, env = EnvVars::UV_LOCKED, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["frozen", "upgrade"])] + #[arg(long, conflicts_with_all = ["frozen", "upgrade"])] pub locked: bool, - /// Run without updating the `uv.lock` file. + /// Run without updating the `uv.lock` file [env: UV_FROZEN=] /// /// Instead of checking if the lockfile is up-to-date, uses the versions in the lockfile as the /// source of truth. If the lockfile is missing, uv will exit with an error. If the /// `pyproject.toml` includes changes to dependencies that have not been included in the /// lockfile yet, they will not be present in the environment. - #[arg(long, env = EnvVars::UV_FROZEN, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["locked", "upgrade", "no_sources"])] + #[arg(long, conflicts_with_all = ["locked", "upgrade", "no_sources"])] pub frozen: bool, /// Run the given path as a Python script. @@ -3731,10 +3727,11 @@ pub struct RunArgs { )] pub python: Option>, - /// Whether to show resolver and installer output from any environment modifications. + /// Whether to show resolver and installer output from any environment modifications [env: + /// UV_SHOW_RESOLUTION=] /// /// By default, environment modifications are omitted, but enabled under `--verbose`. - #[arg(long, env = EnvVars::UV_SHOW_RESOLUTION, value_parser = clap::builder::BoolishValueParser::new(), hide = true)] + #[arg(long, value_parser = clap::builder::BoolishValueParser::new(), hide = true)] pub show_resolution: bool, /// Number of times that `uv run` will allow recursive invocations. @@ -3813,17 +3810,17 @@ pub struct SyncArgs { #[arg(long, overrides_with("all_extras"), hide = true)] pub no_all_extras: bool, - /// Include the development dependency group. + /// Include the development dependency group [env: UV_DEV=] /// /// This option is an alias for `--group dev`. - #[arg(long, overrides_with("no_dev"), hide = true, env = EnvVars::UV_DEV, value_parser = clap::builder::BoolishValueParser::new())] + #[arg(long, overrides_with("no_dev"), hide = true, value_parser = clap::builder::BoolishValueParser::new())] pub dev: bool, - /// Disable the development dependency group. + /// Disable the development dependency group [env: UV_NO_DEV=] /// /// This option is an alias of `--no-group dev`. /// See `--no-default-groups` to disable all default groups instead. - #[arg(long, overrides_with("dev"), env = EnvVars::UV_NO_DEV, value_parser = clap::builder::BoolishValueParser::new())] + #[arg(long, overrides_with("dev"), value_parser = clap::builder::BoolishValueParser::new())] pub no_dev: bool, /// Only include the development dependency group. @@ -3879,8 +3876,8 @@ pub struct SyncArgs { pub editable: bool, /// Install any editable dependencies, including the project and any workspace members, as - /// non-editable. - #[arg(long, overrides_with = "editable", value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_NO_EDITABLE)] + /// non-editable [env: UV_NO_EDITABLE=] + #[arg(long, overrides_with = "editable", value_parser = clap::builder::BoolishValueParser::new())] pub no_editable: bool, /// Do not remove extraneous packages present in the environment. @@ -3972,20 +3969,20 @@ pub struct SyncArgs { #[arg(long, conflicts_with = "no_install_package", hide = true, value_hint = ValueHint::Other)] pub only_install_package: Vec, - /// Assert that the `uv.lock` will remain unchanged. + /// Assert that the `uv.lock` will remain unchanged [env: UV_LOCKED=] /// /// Requires that the lockfile is up-to-date. If the lockfile is missing or needs to be updated, /// uv will exit with an error. - #[arg(long, env = EnvVars::UV_LOCKED, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["frozen", "upgrade"])] + #[arg(long, conflicts_with_all = ["frozen", "upgrade"])] pub locked: bool, - /// Sync without updating the `uv.lock` file. + /// Sync without updating the `uv.lock` file [env: UV_FROZEN=] /// /// Instead of checking if the lockfile is up-to-date, uses the versions in the lockfile as the /// source of truth. If the lockfile is missing, uv will exit with an error. If the /// `pyproject.toml` includes changes to dependencies that have not been included in the /// lockfile yet, they will not be present in the environment. - #[arg(long, env = EnvVars::UV_FROZEN, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["locked", "upgrade", "no_sources"])] + #[arg(long, conflicts_with_all = ["locked", "upgrade", "no_sources"])] pub frozen: bool, /// Perform a dry run, without writing the lockfile or modifying the project environment. @@ -4114,19 +4111,19 @@ pub struct LockArgs { #[arg(long, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["check_exists", "upgrade"], overrides_with = "check")] pub check: bool, - /// Check if the lockfile is up-to-date. + /// Check if the lockfile is up-to-date [env: UV_LOCKED=] /// /// Asserts that the `uv.lock` would remain unchanged after a resolution. If the lockfile is /// missing or needs to be updated, uv will exit with an error. /// /// Equivalent to `--check`. - #[arg(long, env = EnvVars::UV_LOCKED, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["check_exists", "upgrade"], hide = true)] + #[arg(long, conflicts_with_all = ["check_exists", "upgrade"], hide = true)] pub locked: bool, - /// Assert that a `uv.lock` exists without checking if it is up-to-date. + /// Assert that a `uv.lock` exists without checking if it is up-to-date [env: UV_FROZEN=] /// /// Equivalent to `--frozen`. - #[arg(long, alias = "frozen", env = EnvVars::UV_FROZEN, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["check", "locked"])] + #[arg(long, alias = "frozen", conflicts_with_all = ["check", "locked"])] pub check_exists: bool, /// Perform a dry run, without writing the lockfile. @@ -4221,7 +4218,7 @@ pub struct AddArgs { #[arg(long, short, value_parser = MarkerTree::from_str, value_hint = ValueHint::Other)] pub marker: Option, - /// Add the requirements to the development dependency group. + /// Add the requirements to the development dependency group [env: UV_DEV=] /// /// This option is an alias for `--group dev`. #[arg( @@ -4229,7 +4226,6 @@ pub struct AddArgs { conflicts_with("optional"), conflicts_with("group"), conflicts_with("script"), - env = EnvVars::UV_DEV, value_parser = clap::builder::BoolishValueParser::new() )] pub dev: bool, @@ -4258,7 +4254,8 @@ pub struct AddArgs { #[arg(long, overrides_with = "no_editable")] pub editable: bool, - #[arg(long, overrides_with = "editable", hide = true, value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_NO_EDITABLE)] + /// Don't add the requirements as editable [env: UV_NO_EDITABLE=] + #[arg(long, overrides_with = "editable", hide = true, value_parser = clap::builder::BoolishValueParser::new())] pub no_editable: bool, /// Add a dependency as provided. @@ -4317,21 +4314,21 @@ pub struct AddArgs { #[arg(long, value_hint = ValueHint::Other)] pub extra: Option>, - /// Avoid syncing the virtual environment. - #[arg(long, env = EnvVars::UV_NO_SYNC, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with = "frozen")] + /// Avoid syncing the virtual environment [env: UV_NO_SYNC=] + #[arg(long)] pub no_sync: bool, - /// Assert that the `uv.lock` will remain unchanged. + /// Assert that the `uv.lock` will remain unchanged [env: UV_LOCKED=] /// /// Requires that the lockfile is up-to-date. If the lockfile is missing or needs to be updated, /// uv will exit with an error. - #[arg(long, env = EnvVars::UV_LOCKED, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["frozen", "upgrade"])] + #[arg(long, conflicts_with_all = ["frozen", "upgrade"])] pub locked: bool, - /// Add dependencies without re-locking the project. + /// Add dependencies without re-locking the project [env: UV_FROZEN=] /// /// The project environment will not be synced. - #[arg(long, env = EnvVars::UV_FROZEN, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["locked", "upgrade", "no_sources"])] + #[arg(long, conflicts_with_all = ["locked", "upgrade", "no_sources"])] pub frozen: bool, /// Prefer the active virtual environment over the project's virtual environment. @@ -4523,10 +4520,10 @@ pub struct RemoveArgs { #[arg(required = true, value_hint = ValueHint::Other)] pub packages: Vec>, - /// Remove the packages from the development dependency group. + /// Remove the packages from the development dependency group [env: UV_DEV=] /// /// This option is an alias for `--group dev`. - #[arg(long, conflicts_with("optional"), conflicts_with("group"), env = EnvVars::UV_DEV, value_parser = clap::builder::BoolishValueParser::new())] + #[arg(long, conflicts_with("optional"), conflicts_with("group"), value_parser = clap::builder::BoolishValueParser::new())] pub dev: bool, /// Remove the packages from the project's optional dependencies for the specified extra. @@ -4549,8 +4546,8 @@ pub struct RemoveArgs { )] pub group: Option, - /// Avoid syncing the virtual environment after re-locking the project. - #[arg(long, env = EnvVars::UV_NO_SYNC, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with = "frozen")] + /// Avoid syncing the virtual environment after re-locking the project [env: UV_NO_SYNC=] + #[arg(long)] pub no_sync: bool, /// Prefer the active virtual environment over the project's virtual environment. @@ -4566,17 +4563,17 @@ pub struct RemoveArgs { #[arg(long, overrides_with = "active", hide = true)] pub no_active: bool, - /// Assert that the `uv.lock` will remain unchanged. + /// Assert that the `uv.lock` will remain unchanged [env: UV_LOCKED=] /// /// Requires that the lockfile is up-to-date. If the lockfile is missing or needs to be updated, /// uv will exit with an error. - #[arg(long, env = EnvVars::UV_LOCKED, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["frozen", "upgrade"])] + #[arg(long, conflicts_with_all = ["frozen", "upgrade"])] pub locked: bool, - /// Remove dependencies without re-locking the project. + /// Remove dependencies without re-locking the project [env: UV_FROZEN=] /// /// The project environment will not be synced. - #[arg(long, env = EnvVars::UV_FROZEN, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["locked", "upgrade", "no_sources"])] + #[arg(long, conflicts_with_all = ["locked", "upgrade", "no_sources"])] pub frozen: bool, #[command(flatten)] @@ -4628,13 +4625,13 @@ pub struct TreeArgs { #[command(flatten)] pub tree: DisplayTreeArgs, - /// Include the development dependency group. + /// Include the development dependency group [env: UV_DEV=] /// /// Development dependencies are defined via `dependency-groups.dev` or /// `tool.uv.dev-dependencies` in a `pyproject.toml`. /// /// This option is an alias for `--group dev`. - #[arg(long, overrides_with("no_dev"), hide = true, env = EnvVars::UV_DEV, value_parser = clap::builder::BoolishValueParser::new())] + #[arg(long, overrides_with("no_dev"), hide = true, value_parser = clap::builder::BoolishValueParser::new())] pub dev: bool, /// Only include the development dependency group. @@ -4645,11 +4642,11 @@ pub struct TreeArgs { #[arg(long, conflicts_with_all = ["group", "all_groups", "no_dev"])] pub only_dev: bool, - /// Disable the development dependency group. + /// Disable the development dependency group [env: UV_NO_DEV=] /// /// This option is an alias of `--no-group dev`. /// See `--no-default-groups` to disable all default groups instead. - #[arg(long, overrides_with("dev"), env = EnvVars::UV_NO_DEV, value_parser = clap::builder::BoolishValueParser::new())] + #[arg(long, overrides_with("dev"), value_parser = clap::builder::BoolishValueParser::new())] pub no_dev: bool, /// Include dependencies from the specified dependency group. @@ -4688,17 +4685,17 @@ pub struct TreeArgs { #[arg(long, conflicts_with_all = ["only_group", "only_dev"])] pub all_groups: bool, - /// Assert that the `uv.lock` will remain unchanged. + /// Assert that the `uv.lock` will remain unchanged [env: UV_LOCKED=] /// /// Requires that the lockfile is up-to-date. If the lockfile is missing or needs to be updated, /// uv will exit with an error. - #[arg(long, env = EnvVars::UV_LOCKED, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["frozen", "upgrade"])] + #[arg(long, conflicts_with_all = ["frozen", "upgrade"])] pub locked: bool, - /// Display the requirements without locking the project. + /// Display the requirements without locking the project [env: UV_FROZEN=] /// /// If the lockfile is missing, uv will exit with an error. - #[arg(long, env = EnvVars::UV_FROZEN, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["locked", "upgrade", "no_sources"])] + #[arg(long, conflicts_with_all = ["locked", "upgrade", "no_sources"])] pub frozen: bool, #[command(flatten)] @@ -4808,17 +4805,17 @@ pub struct ExportArgs { #[arg(long, overrides_with("all_extras"), hide = true)] pub no_all_extras: bool, - /// Include the development dependency group. + /// Include the development dependency group [env: UV_DEV=] /// /// This option is an alias for `--group dev`. - #[arg(long, overrides_with("no_dev"), hide = true, env = EnvVars::UV_DEV, value_parser = clap::builder::BoolishValueParser::new())] + #[arg(long, overrides_with("no_dev"), hide = true, value_parser = clap::builder::BoolishValueParser::new())] pub dev: bool, - /// Disable the development dependency group. + /// Disable the development dependency group [env: UV_NO_DEV=] /// /// This option is an alias of `--no-group dev`. /// See `--no-default-groups` to disable all default groups instead. - #[arg(long, overrides_with("dev"), env = EnvVars::UV_NO_DEV, value_parser = clap::builder::BoolishValueParser::new())] + #[arg(long, overrides_with("dev"), value_parser = clap::builder::BoolishValueParser::new())] pub no_dev: bool, /// Only include the development dependency group. @@ -4885,8 +4882,8 @@ pub struct ExportArgs { pub editable: bool, /// Export any editable dependencies, including the project and any workspace members, as - /// non-editable. - #[arg(long, overrides_with = "editable", value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_NO_EDITABLE)] + /// non-editable [env: UV_NO_EDITABLE=] + #[arg(long, overrides_with = "editable", value_parser = clap::builder::BoolishValueParser::new())] pub no_editable: bool, /// Include hashes for all dependencies. @@ -4994,17 +4991,17 @@ pub struct ExportArgs { )] pub only_emit_package: Vec, - /// Assert that the `uv.lock` will remain unchanged. + /// Assert that the `uv.lock` will remain unchanged [env: UV_LOCKED=] /// /// Requires that the lockfile is up-to-date. If the lockfile is missing or needs to be updated, /// uv will exit with an error. - #[arg(long, env = EnvVars::UV_LOCKED, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["frozen", "upgrade"])] + #[arg(long, conflicts_with_all = ["frozen", "upgrade"])] pub locked: bool, - /// Do not update the `uv.lock` before exporting. + /// Do not update the `uv.lock` before exporting [env: UV_FROZEN=] /// /// If a `uv.lock` does not exist, uv will exit with an error. - #[arg(long, env = EnvVars::UV_FROZEN, value_parser = clap::builder::BoolishValueParser::new(), conflicts_with_all = ["locked", "upgrade", "no_sources"])] + #[arg(long, conflicts_with_all = ["locked", "upgrade", "no_sources"])] pub frozen: bool, #[command(flatten)] @@ -5302,8 +5299,9 @@ pub struct ToolRunArgs { )] pub overrides: Vec>, - /// Run the tool in an isolated virtual environment, ignoring any already-installed tools. - #[arg(long, env = EnvVars::UV_ISOLATED, value_parser = clap::builder::BoolishValueParser::new())] + /// Run the tool in an isolated virtual environment, ignoring any already-installed tools [env: + /// UV_ISOLATED=] + #[arg(long, value_parser = clap::builder::BoolishValueParser::new())] pub isolated: bool, /// Load environment variables from a `.env` file. @@ -5313,8 +5311,8 @@ pub struct ToolRunArgs { #[arg(long, value_delimiter = ' ', env = EnvVars::UV_ENV_FILE, value_hint = ValueHint::FilePath)] pub env_file: Vec, - /// Avoid reading environment variables from a `.env` file. - #[arg(long, value_parser = clap::builder::BoolishValueParser::new(), env = EnvVars::UV_NO_ENV_FILE)] + /// Avoid reading environment variables from a `.env` file [env: UV_NO_ENV_FILE=] + #[arg(long, value_parser = clap::builder::BoolishValueParser::new())] pub no_env_file: bool, #[command(flatten)] @@ -5344,10 +5342,11 @@ pub struct ToolRunArgs { )] pub python: Option>, - /// Whether to show resolver and installer output from any environment modifications. + /// Whether to show resolver and installer output from any environment modifications [env: + /// UV_SHOW_RESOLUTION=] /// /// By default, environment modifications are omitted, but enabled under `--verbose`. - #[arg(long, env = EnvVars::UV_SHOW_RESOLUTION, value_parser = clap::builder::BoolishValueParser::new(), hide = true)] + #[arg(long, value_parser = clap::builder::BoolishValueParser::new(), hide = true)] pub show_resolution: bool, /// The platform for which requirements should be installed. @@ -6649,17 +6648,11 @@ pub struct IndexArgs { #[derive(Args)] pub struct RefreshArgs { /// Refresh all cached data. - #[arg( - long, - conflicts_with("offline"), - overrides_with("no_refresh"), - help_heading = "Cache options" - )] + #[arg(long, overrides_with("no_refresh"), help_heading = "Cache options")] pub refresh: bool, #[arg( long, - conflicts_with("offline"), overrides_with("refresh"), hide = true, help_heading = "Cache options" diff --git a/crates/uv-cli/src/options.rs b/crates/uv-cli/src/options.rs index f8d492f554a39..f753fdbf901b1 100644 --- a/crates/uv-cli/src/options.rs +++ b/crates/uv-cli/src/options.rs @@ -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::{ @@ -37,6 +39,150 @@ pub fn flag(yes: bool, no: bool, name: &str) -> Option { } } +/// 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 { + 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 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 for Refresh { fn from(value: RefreshArgs) -> Self { let RefreshArgs { diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs index 50c185a63f4af..a4d2f60305570 100644 --- a/crates/uv-settings/src/lib.rs +++ b/crates/uv-settings/src/lib.rs @@ -582,6 +582,25 @@ pub struct Concurrency { pub installs: Option, } +/// 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, + 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 { + 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 @@ -601,6 +620,24 @@ pub struct EnvironmentOptions { pub concurrency: Concurrency, #[cfg(feature = "tracing-durations-export")] pub tracing_durations_file: Option, + 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 { @@ -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)?, }) } } diff --git a/crates/uv/src/commands/help.rs b/crates/uv/src/commands/help.rs index acf8c49d65d3b..c920dbcb8b8ed 100644 --- a/crates/uv/src/commands/help.rs +++ b/crates/uv/src/commands/help.rs @@ -70,6 +70,18 @@ pub(crate) fn help(query: &[String], printer: Printer, no_pager: bool) -> Result .render_long_help() }; + // Reformat inline [env: VAR=] annotations to their own line. + let help_plain = if is_root { + help.to_string() + } else { + reformat_env_annotations(&help.to_string()) + }; + let help_ansi = if is_root { + help.ansi().to_string() + } else { + reformat_env_annotations(&help.ansi().to_string()) + }; + let want_color = match anstream::Stdout::choice(&std::io::stdout()) { ColorChoice::Always | ColorChoice::AlwaysAnsi => true, ColorChoice::Never => false, @@ -83,21 +95,156 @@ pub(crate) fn help(query: &[String], printer: Printer, no_pager: bool) -> Result if should_page && let Some(pager) = Pager::try_from_env() { let query = query.join(" "); if want_color && pager.supports_colors() { - pager.spawn(format!("{}: {query}", "uv help".bold()), help.ansi())?; + pager.spawn(format!("{}: {query}", "uv help".bold()), &help_ansi)?; } else { - pager.spawn(format!("uv help: {query}"), help)?; + pager.spawn(format!("uv help: {query}"), &help_plain)?; } } else { if want_color { - writeln!(printer.stdout(), "{}", help.ansi())?; + writeln!(printer.stdout(), "{help_ansi}")?; } else { - writeln!(printer.stdout(), "{help}")?; + writeln!(printer.stdout(), "{help_plain}")?; } } Ok(ExitStatus::Success) } +/// Get the first non-ANSI character starting at a given byte position. +/// +/// Returns `None` if the rest of the string is empty or only contains ANSI sequences. +fn first_non_ansi_char(s: &str, start: usize) -> Option { + let mut chars = s[start..].chars().peekable(); + while let Some(c) = chars.next() { + if c == '\x1b' { + // Skip ANSI escape sequences. + if chars.peek() == Some(&'[') { + chars.next(); + for c in chars.by_ref() { + if c.is_ascii_alphabetic() { + break; + } + } + } + } else { + return Some(c); + } + } + None +} + +/// Reformat `[env: VAR=]` annotations in long help output. +/// +/// Moves inline `[env: VAR=]` annotations to their own line at the end of each +/// argument's description, matching clap's native formatting for environment vars. +fn reformat_env_annotations(help: &str) -> String { + let mut result = String::new(); + let mut pending_env: Option = None; + + let lines: Vec<&str> = help.lines().collect(); + let mut i = 0; + + while i < lines.len() { + let line = lines[i]; + + // Classify the line type based on clap's help formatting: + // - Argument lines: 6 spaces + `-` or `<` (e.g., " --offline", " ") + // - Description lines: 10 spaces + text (e.g., " Disable network access") + // - Section headers: no leading spaces, ends with `:` (e.g., "Options:") + // + // Leading spaces never contain ANSI codes, but argument names may be colored, + // so we skip ANSI sequences when checking the first content character. + let indent = line.len() - line.trim_start().len(); + let first_char = first_non_ansi_char(line, indent); + let is_arg_line = indent == 6 && matches!(first_char, Some('-' | '<')); + let is_section_header = indent == 0 && line.ends_with(':'); + let is_description_line = indent == 10; + + // Flush pending env before starting a new argument or section. + if is_arg_line || is_section_header { + if let Some(env) = pending_env.take() { + // Remove trailing blank lines; add exactly one blank line before the environment variable. + while result.ends_with("\n\n") { + result.pop(); + } + if !result.ends_with('\n') { + result.push('\n'); + } + result.push('\n'); + let _ = write!(result, " {env}\n\n"); + } + } + + // Check for inline environment annotations on description lines. + if is_description_line { + if let Some((env_annotation, new_line)) = extract_env_annotation(line) { + pending_env = Some(env_annotation); + if !new_line.trim().is_empty() { + result.push_str(&new_line); + // Add a period, if the line doesn't end with punctuation. + if !new_line.ends_with('.') && !new_line.ends_with(':') { + result.push('.'); + } + result.push('\n'); + } + i += 1; + continue; + } + } + + result.push_str(line); + result.push('\n'); + i += 1; + } + + // Flush any remaining pending environment variables at the end of the help. + if let Some(env) = pending_env { + while result.ends_with("\n\n") { + result.pop(); + } + if !result.ends_with('\n') { + result.push('\n'); + } + result.push('\n'); + let _ = writeln!(result, " {env}"); + } + + if result.ends_with('\n') { + result.pop(); + } + + result +} + +/// Extract an inline `[env: VAR=]` annotation from a line. +/// +/// Returns the annotation and the line with the annotation removed, or `None` if no +/// annotation is found. +fn extract_env_annotation(line: &str) -> Option<(String, String)> { + // Look for the pattern: " [env: SOMETHING=]" + let start = line.find(" [env: ")?; + let rest = &line[start + " [env: ".len()..]; + let end_offset = rest.find("=]")?; + + // Validate that the environment variable name contains only uppercase letters and underscores. + let env_name = &rest[..end_offset]; + if !env_name.chars().all(|c| c.is_ascii_uppercase() || c == '_') { + return None; + } + + let annotation_end = start + " [env: ".len() + end_offset + "=]".len(); + let annotation = line[start + " ".len()..annotation_end].to_string(); + let new_line = format!("{}{}", &line[..start], &line[annotation_end..]); + + // Only extract if there's actual text remaining (not just whitespace). + // If the line is just the annotation (clap-generated), leave it alone. + if new_line.trim().is_empty() { + return None; + } + + Some((annotation, new_line)) +} + /// Find the command corresponding to a set of arguments, e.g., `["uv", "pip", "install"]`. /// /// If the command cannot be found, the nearest command is returned. diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 28f7ee158e10b..1868be06fc55d 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -57,14 +57,14 @@ use crate::commands::project::{ use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter}; use crate::commands::{ExitStatus, ScriptPath, diagnostics, project}; use crate::printer::Printer; -use crate::settings::{LockCheck, ResolverInstallerSettings}; +use crate::settings::{FrozenSource, LockCheck, ResolverInstallerSettings}; /// Add one or more packages to the project requirements. #[allow(clippy::fn_params_excessive_bools)] pub(crate) async fn add( project_dir: &Path, lock_check: LockCheck, - frozen: bool, + frozen: Option, active: Option, no_sync: bool, no_install_project: bool, @@ -187,7 +187,7 @@ pub(crate) async fn add( "`{lock_check}` is a no-op for Python scripts with inline metadata, which always run in isolation" ); } - if frozen { + if frozen.is_some() { warn_user_once!( "`--frozen` is a no-op for Python scripts with inline metadata, which always run in isolation" ); @@ -291,7 +291,7 @@ pub(crate) async fn add( defaulted_groups = groups.with_defaults(default_dependency_groups(project.pyproject_toml())?); - if frozen || no_sync { + if frozen.is_some() || no_sync { // Discover the interpreter. let interpreter = ProjectInterpreter::discover( project.workspace(), @@ -706,7 +706,7 @@ pub(crate) async fn add( // If `--frozen`, exit early. There's no reason to lock and sync, since we don't need a `uv.lock` // to exist at all. - if frozen { + if frozen.is_some() { return Ok(ExitStatus::Success); } diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index 704d65f1fa239..542ed9e85154f 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -27,12 +27,12 @@ use crate::commands::project::install_target::InstallTarget; use crate::commands::project::lock::{LockMode, LockOperation}; use crate::commands::project::lock_target::LockTarget; use crate::commands::project::{ - MissingLockfileSource, ProjectError, ProjectInterpreter, ScriptInterpreter, UniversalState, - default_dependency_groups, detect_conflicts, + ProjectError, ProjectInterpreter, ScriptInterpreter, UniversalState, default_dependency_groups, + detect_conflicts, }; use crate::commands::{ExitStatus, OutputWriter, diagnostics}; use crate::printer::Printer; -use crate::settings::{LockCheck, ResolverSettings}; +use crate::settings::{FrozenSource, LockCheck, ResolverSettings}; #[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] @@ -68,7 +68,7 @@ pub(crate) async fn export( groups: DependencyGroups, editable: Option, lock_check: LockCheck, - frozen: bool, + frozen: Option, include_annotations: bool, include_header: bool, script: Option, @@ -90,7 +90,7 @@ pub(crate) async fn export( let target = if let Some(script) = script { ExportTarget::Script(script) } else { - let project = if frozen { + let project = if frozen.is_some() { VirtualProject::discover( project_dir, &DiscoveryOptions { @@ -142,7 +142,7 @@ pub(crate) async fn export( let extras = extras.with_defaults(default_extras); // Find an interpreter for the project, unless `--frozen` is set. - let interpreter = if frozen { + let interpreter = if frozen.is_some() { None } else { Some(match &target { @@ -184,8 +184,8 @@ pub(crate) async fn export( }; // Determine the lock mode. - let mode = if frozen { - LockMode::Frozen(MissingLockfileSource::frozen()) + let mode = if let Some(frozen_source) = frozen { + LockMode::Frozen(frozen_source.into()) } else if let LockCheck::Enabled(lock_check) = lock_check { LockMode::Locked(interpreter.as_ref().unwrap(), lock_check) } else if matches!(target, ExportTarget::Script(_)) diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 6958f1556e29a..dc2456733ea67 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -49,7 +49,7 @@ use crate::commands::project::{ use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter}; use crate::commands::{ExitStatus, ScriptPath, diagnostics, pip}; use crate::printer::Printer; -use crate::settings::{LockCheck, LockCheckSource, ResolverSettings}; +use crate::settings::{FrozenSource, LockCheck, LockCheckSource, ResolverSettings}; /// The result of running a lock operation. #[derive(Debug, Clone)] @@ -82,7 +82,7 @@ impl LockResult { pub(crate) async fn lock( project_dir: &Path, lock_check: LockCheck, - frozen: bool, + frozen: Option, dry_run: DryRun, refresh: Refresh, python: Option, @@ -136,8 +136,8 @@ pub(crate) async fn lock( // Determine the lock mode. let interpreter; - let mode = if frozen { - LockMode::Frozen(MissingLockfileSource::frozen()) + let mode = if let Some(frozen_source) = frozen { + LockMode::Frozen(frozen_source.into()) } else { interpreter = match target { LockTarget::Workspace(workspace) => ProjectInterpreter::discover( diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index eb4dd2f9674c8..50bfec5f03888 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -41,7 +41,7 @@ use uv_resolver::{ ResolverEnvironment, ResolverOutput, }; use uv_scripts::Pep723ItemRef; -use uv_settings::{PythonInstallMirrors, parse_boolish_environment_variable}; +use uv_settings::PythonInstallMirrors; use uv_static::EnvVars; use uv_torch::{TorchSource, TorchStrategy}; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy}; @@ -58,7 +58,8 @@ use crate::commands::reporters::{PythonDownloadReporter, ResolverReporter}; use crate::commands::{capitalize, conjunction, pip}; use crate::printer::Printer; use crate::settings::{ - InstallerSettingsRef, LockCheckSource, ResolverInstallerSettings, ResolverSettings, + FrozenSource, InstallerSettingsRef, LockCheckSource, ResolverInstallerSettings, + ResolverSettings, }; pub(crate) mod add; @@ -82,10 +83,14 @@ pub(crate) enum MissingLockfileSource { Frozen, /// The `UV_FROZEN` environment variable was set. FrozenEnv, + /// The `frozen` option was set via workspace configuration. + FrozenConfiguration, /// The `--locked` flag was provided. Locked, /// The `UV_LOCKED` environment variable was set. LockedEnv, + /// The `locked` option was set via workspace configuration. + LockedConfiguration, /// The `--check` flag was provided. Check, } @@ -95,8 +100,10 @@ impl std::fmt::Display for MissingLockfileSource { match self { Self::Frozen => write!(f, "`--frozen`"), Self::FrozenEnv => write!(f, "`UV_FROZEN=1`"), + Self::FrozenConfiguration => write!(f, "`frozen` (workspace configuration)"), Self::Locked => write!(f, "`--locked`"), Self::LockedEnv => write!(f, "`UV_LOCKED=1`"), + Self::LockedConfiguration => write!(f, "`locked` (workspace configuration)"), Self::Check => write!(f, "`--check`"), } } @@ -105,38 +112,20 @@ impl std::fmt::Display for MissingLockfileSource { impl From for MissingLockfileSource { fn from(source: LockCheckSource) -> Self { match source { - LockCheckSource::Locked => { - // TODO(charlie): Track the source (flag vs. environment variable) when resolving - // settings, rather than checking after-the-fact. - if matches!( - parse_boolish_environment_variable(EnvVars::UV_LOCKED), - Ok(Some(true)) - ) { - Self::LockedEnv - } else { - Self::Locked - } - } + LockCheckSource::LockedCli => Self::Locked, + LockCheckSource::LockedEnv => Self::LockedEnv, + LockCheckSource::LockedConfiguration => Self::LockedConfiguration, LockCheckSource::Check => Self::Check, } } } -impl MissingLockfileSource { - /// Determine the source of the frozen flag. - /// - /// If `UV_FROZEN` is set to a truthy value in the environment, the source is the environment - /// variable. Otherwise, the source is the `--frozen` flag. - pub(crate) fn frozen() -> Self { - // TODO(charlie): Track the source (flag vs. environment variable) when resolving - // settings, rather than checking after-the-fact. - if matches!( - parse_boolish_environment_variable(EnvVars::UV_FROZEN), - Ok(Some(true)) - ) { - Self::FrozenEnv - } else { - Self::Frozen +impl From for MissingLockfileSource { + fn from(source: FrozenSource) -> Self { + match source { + FrozenSource::Cli => Self::Frozen, + FrozenSource::Env => Self::FrozenEnv, + FrozenSource::Configuration => Self::FrozenConfiguration, } } } diff --git a/crates/uv/src/commands/project/remove.rs b/crates/uv/src/commands/project/remove.rs index 6b79cd7105f09..c8696b21db345 100644 --- a/crates/uv/src/commands/project/remove.rs +++ b/crates/uv/src/commands/project/remove.rs @@ -36,14 +36,14 @@ use crate::commands::project::{ }; use crate::commands::{ExitStatus, diagnostics, project}; use crate::printer::Printer; -use crate::settings::{LockCheck, ResolverInstallerSettings}; +use crate::settings::{FrozenSource, LockCheck, ResolverInstallerSettings}; /// Remove one or more packages from the project requirements. #[allow(clippy::fn_params_excessive_bools)] pub(crate) async fn remove( project_dir: &Path, lock_check: LockCheck, - frozen: bool, + frozen: Option, active: Option, no_sync: bool, packages: Vec, @@ -75,7 +75,7 @@ pub(crate) async fn remove( "`{lock_check}` is a no-op for Python scripts with inline metadata, which always run in isolation", ); } - if frozen { + if frozen.is_some() { warn_user_once!( "`--frozen` is a no-op for Python scripts with inline metadata, which always run in isolation" ); @@ -184,7 +184,7 @@ pub(crate) async fn remove( // If `--frozen`, exit early. There's no reason to lock and sync, since we don't need a `uv.lock` // to exist at all. - if frozen { + if frozen.is_some() { return Ok(ExitStatus::Success); } diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index d1450a7d454a0..ccd6e2b0bce86 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -65,15 +65,15 @@ use crate::commands::project::install_target::InstallTarget; use crate::commands::project::lock::LockMode; use crate::commands::project::lock_target::LockTarget; use crate::commands::project::{ - EnvironmentSpecification, MissingLockfileSource, PreferenceLocation, ProjectEnvironment, - ProjectError, ScriptEnvironment, ScriptInterpreter, UniversalState, WorkspacePython, + EnvironmentSpecification, PreferenceLocation, ProjectEnvironment, ProjectError, + ScriptEnvironment, ScriptInterpreter, UniversalState, WorkspacePython, default_dependency_groups, script_extra_build_requires, script_specification, update_environment, validate_project_requires_python, }; use crate::commands::reporters::PythonDownloadReporter; use crate::commands::{ExitStatus, diagnostics, project}; use crate::printer::Printer; -use crate::settings::{LockCheck, ResolverInstallerSettings, ResolverSettings}; +use crate::settings::{FrozenSource, LockCheck, ResolverInstallerSettings, ResolverSettings}; /// Run a command. #[allow(clippy::fn_params_excessive_bools)] @@ -84,7 +84,7 @@ pub(crate) async fn run( requirements: Vec, show_resolution: bool, lock_check: LockCheck, - frozen: bool, + frozen: Option, active: Option, no_sync: bool, isolated: bool, @@ -262,8 +262,8 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl .ok(); // Determine the lock mode. - let mode = if frozen { - LockMode::Frozen(MissingLockfileSource::frozen()) + let mode = if let Some(frozen_source) = frozen { + LockMode::Frozen(frozen_source.into()) } else if let LockCheck::Enabled(lock_check) = lock_check { LockMode::Locked(environment.interpreter(), lock_check) } else { @@ -364,7 +364,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl "uv lock --script".green(), ); } - if frozen { + if frozen.is_some() { warn_user!( "No lockfile found for Python script (ignoring `--frozen`); run `{}` to generate a lockfile", "uv lock --script".green(), @@ -601,7 +601,7 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl if let LockCheck::Enabled(lock_check) = lock_check { warn_user!("`{lock_check}` has no effect when used alongside `--no-project`"); } - if frozen { + if frozen.is_some() { warn_user!("`--frozen` has no effect when used alongside `--no-project`"); } if no_sync { @@ -748,8 +748,8 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl .ok(); // Determine the lock mode. - let mode = if frozen { - LockMode::Frozen(MissingLockfileSource::frozen()) + let mode = if let Some(frozen_source) = frozen { + LockMode::Frozen(frozen_source.into()) } else if let LockCheck::Enabled(lock_check) = lock_check { LockMode::Locked(venv.interpreter(), lock_check) } else if isolated { diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index f2356900758ad..87bd91952f2e4 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -44,14 +44,15 @@ use crate::commands::project::install_target::InstallTarget; use crate::commands::project::lock::{LockMode, LockOperation, LockResult}; use crate::commands::project::lock_target::LockTarget; use crate::commands::project::{ - EnvironmentUpdate, MissingLockfileSource, PlatformState, ProjectEnvironment, ProjectError, - ScriptEnvironment, UniversalState, default_dependency_groups, detect_conflicts, - script_extra_build_requires, script_specification, update_environment, + EnvironmentUpdate, PlatformState, ProjectEnvironment, ProjectError, ScriptEnvironment, + UniversalState, default_dependency_groups, detect_conflicts, script_extra_build_requires, + script_specification, update_environment, }; use crate::commands::{ExitStatus, diagnostics}; use crate::printer::Printer; use crate::settings::{ - InstallerSettingsRef, LockCheck, LockCheckSource, ResolverInstallerSettings, ResolverSettings, + FrozenSource, InstallerSettingsRef, LockCheck, LockCheckSource, ResolverInstallerSettings, + ResolverSettings, }; /// Sync the project environment. @@ -59,7 +60,7 @@ use crate::settings::{ pub(crate) async fn sync( project_dir: &Path, lock_check: LockCheck, - frozen: bool, + frozen: Option, dry_run: DryRun, active: Option, all_packages: bool, @@ -99,7 +100,7 @@ pub(crate) async fn sync( SyncTarget::Script(script) } else { // Identify the project. - let project = if frozen { + let project = if frozen.is_some() { VirtualProject::discover( project_dir, &DiscoveryOptions { @@ -225,7 +226,7 @@ pub(crate) async fn sync( if let SyncTarget::Script(script) = &target { let lockfile = LockTarget::from(script).lock_path(); if !lockfile.is_file() { - if frozen { + if frozen.is_some() { return Err(anyhow::anyhow!( "`uv sync --frozen` requires a script lockfile; run `{}` to lock the script", format!("uv lock --script {}", script.path.user_display()).green(), @@ -329,8 +330,8 @@ pub(crate) async fn sync( let state = UniversalState::default(); // Determine the lock mode. - let mode = if frozen { - LockMode::Frozen(MissingLockfileSource::frozen()) + let mode = if let Some(frozen_source) = frozen { + LockMode::Frozen(frozen_source.into()) } else if let LockCheck::Enabled(lock_check) = lock_check { LockMode::Locked(environment.interpreter(), lock_check) } else if dry_run.enabled() { diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs index bc86c87479c8e..198b9b6531474 100644 --- a/crates/uv/src/commands/project/tree.rs +++ b/crates/uv/src/commands/project/tree.rs @@ -24,12 +24,12 @@ use crate::commands::pip::resolution_markers; use crate::commands::project::lock::{LockMode, LockOperation}; use crate::commands::project::lock_target::LockTarget; use crate::commands::project::{ - MissingLockfileSource, ProjectError, ProjectInterpreter, ScriptInterpreter, UniversalState, - default_dependency_groups, + ProjectError, ProjectInterpreter, ScriptInterpreter, UniversalState, default_dependency_groups, }; use crate::commands::reporters::LatestVersionReporter; use crate::commands::{ExitStatus, diagnostics}; use crate::printer::Printer; +use crate::settings::FrozenSource; use crate::settings::LockCheck; use crate::settings::ResolverSettings; @@ -39,7 +39,7 @@ pub(crate) async fn tree( project_dir: &Path, groups: DependencyGroups, lock_check: LockCheck, - frozen: bool, + frozen: Option, universal: bool, depth: u8, prune: Vec, @@ -83,7 +83,7 @@ pub(crate) async fn tree( let groups = groups.with_defaults(default_groups); // Find an interpreter for the project, unless `--frozen` and `--universal` are both set. - let interpreter = if frozen && universal { + let interpreter = if frozen.is_some() && universal { None } else { Some(match target { @@ -125,8 +125,8 @@ pub(crate) async fn tree( }; // Determine the lock mode. - let mode = if frozen { - LockMode::Frozen(MissingLockfileSource::frozen()) + let mode = if let Some(frozen_source) = frozen { + LockMode::Frozen(frozen_source.into()) } else if let LockCheck::Enabled(lock_check) = lock_check { LockMode::Locked(interpreter.as_ref().unwrap(), lock_check) } else if matches!(target, LockTarget::Script(_)) && !target.lock_path().is_file() { diff --git a/crates/uv/src/commands/project/version.rs b/crates/uv/src/commands/project/version.rs index 725a819145348..70744ac251103 100644 --- a/crates/uv/src/commands/project/version.rs +++ b/crates/uv/src/commands/project/version.rs @@ -34,12 +34,11 @@ use crate::commands::project::add::{AddTarget, PythonTarget}; use crate::commands::project::install_target::InstallTarget; use crate::commands::project::lock::LockMode; use crate::commands::project::{ - MissingLockfileSource, ProjectEnvironment, ProjectError, ProjectInterpreter, UniversalState, - default_dependency_groups, + ProjectEnvironment, ProjectError, ProjectInterpreter, UniversalState, default_dependency_groups, }; use crate::commands::{ExitStatus, diagnostics, project}; use crate::printer::Printer; -use crate::settings::{LockCheck, ResolverInstallerSettings}; +use crate::settings::{FrozenSource, LockCheck, ResolverInstallerSettings}; /// Display version information for uv itself (`uv self version`) pub(crate) fn self_version( @@ -65,7 +64,7 @@ pub(crate) async fn project_version( explicit_project: bool, dry_run: bool, lock_check: LockCheck, - frozen: bool, + frozen: Option, active: Option, no_sync: bool, python: Option, @@ -94,27 +93,30 @@ pub(crate) async fn project_version( // Short-circuit early for a frozen read let is_read_only = value.is_none() && bump.is_empty(); - if frozen && is_read_only { - return Box::pin(print_frozen_version( - project, - &name, - project_dir, - active, - python, - install_mirrors, - &settings, - client_builder, - python_preference, - python_downloads, - concurrency, - no_config, - cache, - short, - output_format, - printer, - preview, - )) - .await; + if let Some(frozen_source) = frozen { + if is_read_only { + return Box::pin(print_frozen_version( + project, + &name, + project_dir, + frozen_source, + active, + python, + install_mirrors, + &settings, + client_builder, + python_preference, + python_downloads, + concurrency, + no_config, + cache, + short, + output_format, + printer, + preview, + )) + .await; + } } let mut toml = PyProjectTomlMut::from_toml( @@ -422,6 +424,7 @@ async fn print_frozen_version( project: VirtualProject, name: &PackageName, project_dir: &Path, + frozen_source: FrozenSource, active: Option, python: Option, install_mirrors: PythonInstallMirrors, @@ -465,7 +468,7 @@ async fn print_frozen_version( // Lock and sync the environment, if necessary. let lock = match Box::pin( project::lock::LockOperation::new( - LockMode::Frozen(MissingLockfileSource::frozen()), + LockMode::Frozen(frozen_source.into()), &settings.resolver, &client_builder, &state, @@ -518,7 +521,7 @@ async fn lock_and_sync( project: VirtualProject, project_dir: &Path, lock_check: LockCheck, - frozen: bool, + frozen: Option, active: Option, no_sync: bool, python: Option, @@ -535,7 +538,7 @@ async fn lock_and_sync( preview: Preview, ) -> Result { // If frozen, don't touch the lock or sync at all - if frozen { + if frozen.is_some() { return Ok(ExitStatus::Success); } diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index de61d7817af98..c85623cdcf144 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -561,6 +561,11 @@ async fn run(mut cli: Cli) -> Result { let args = PipCompileSettings::resolve(args, filesystem, environment); show_settings!(args); + // Check for conflicts between offline and refresh. + globals + .network_settings + .check_refresh_conflict(&args.refresh); + // Initialize the cache. let cache = cache.init().await?.with_refresh( args.refresh @@ -671,6 +676,11 @@ async fn run(mut cli: Cli) -> Result { let args = PipSyncSettings::resolve(args, filesystem, environment); show_settings!(args); + // Check for conflicts between offline and refresh. + globals + .network_settings + .check_refresh_conflict(&args.refresh); + // Initialize the cache. let cache = cache.init().await?.with_refresh( args.refresh @@ -834,6 +844,11 @@ async fn run(mut cli: Cli) -> Result { } } + // Check for conflicts between offline and refresh. + globals + .network_settings + .check_refresh_conflict(&args.refresh); + // Initialize the cache. let cache = cache.init().await?.with_refresh( args.refresh @@ -1095,6 +1110,11 @@ async fn run(mut cli: Cli) -> Result { let args = settings::BuildSettings::resolve(args, filesystem, environment); show_settings!(args); + // Check for conflicts between offline and refresh. + globals + .network_settings + .check_refresh_conflict(&args.refresh); + // Initialize the cache. let cache = cache.init().await?.with_refresh( args.refresh @@ -1156,6 +1176,11 @@ async fn run(mut cli: Cli) -> Result { let args = settings::VenvSettings::resolve(args, filesystem, environment); show_settings!(args); + // Check for conflicts between offline and refresh. + globals + .network_settings + .check_refresh_conflict(&args.refresh); + // Initialize the cache. let cache = cache.init().await?.with_refresh( args.refresh @@ -1319,6 +1344,11 @@ async fn run(mut cli: Cli) -> Result { ); show_settings!(args); + // Check for conflicts between offline and refresh. + globals + .network_settings + .check_refresh_conflict(&args.refresh); + // Initialize the cache. let cache = cache.init().await?.with_refresh( args.refresh @@ -1404,6 +1434,11 @@ async fn run(mut cli: Cli) -> Result { let args = settings::ToolInstallSettings::resolve(args, filesystem, environment); show_settings!(args); + // Check for conflicts between offline and refresh. + globals + .network_settings + .check_refresh_conflict(&args.refresh); + // Initialize the cache. let cache = cache.init().await?.with_refresh( args.refresh @@ -1945,6 +1980,11 @@ async fn run_project( let args = settings::RunSettings::resolve(args, filesystem, environment); show_settings!(args); + // Check for conflicts between offline and refresh. + globals + .network_settings + .check_refresh_conflict(&args.refresh); + // Initialize the cache. let cache = cache.init().await?.with_refresh( args.refresh @@ -2009,6 +2049,11 @@ async fn run_project( let args = settings::SyncSettings::resolve(args, filesystem, environment); show_settings!(args); + // Check for conflicts between offline and refresh. + globals + .network_settings + .check_refresh_conflict(&args.refresh); + // Initialize the cache. let cache = cache.init().await?.with_refresh( args.refresh @@ -2059,6 +2104,11 @@ async fn run_project( let args = settings::LockSettings::resolve(args, filesystem, environment); show_settings!(args); + // Check for conflicts between offline and refresh. + globals + .network_settings + .check_refresh_conflict(&args.refresh); + // Initialize the cache. let cache = cache.init().await?.with_refresh( args.refresh @@ -2171,6 +2221,11 @@ async fn run_project( } } + // Check for conflicts between offline and refresh. + globals + .network_settings + .check_refresh_conflict(&args.refresh); + // Initialize the cache. let cache = cache.init().await?.with_refresh( args.refresh @@ -2234,6 +2289,11 @@ async fn run_project( let args = settings::RemoveSettings::resolve(args, filesystem, environment); show_settings!(args); + // Check for conflicts between offline and refresh. + globals + .network_settings + .check_refresh_conflict(&args.refresh); + // Initialize the cache. let cache = cache.init().await?.with_refresh( args.refresh @@ -2278,6 +2338,11 @@ async fn run_project( let args = settings::VersionSettings::resolve(args, filesystem, environment); show_settings!(args); + // Check for conflicts between offline and refresh. + globals + .network_settings + .check_refresh_conflict(&args.refresh); + // Initialize the cache. let cache = cache.init().await?.with_refresh( args.refresh diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index d69da18fd8708..8339279dc9e64 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -21,7 +21,10 @@ use uv_cli::{ use uv_cli::{ AuthorFrom, BuildArgs, ExportArgs, FormatArgs, PublishArgs, PythonDirArgs, ResolverInstallerArgs, ToolUpgradeArgs, - options::{flag, resolver_installer_options, resolver_options}, + options::{ + Flag, FlagSource, check_conflicts, flag, resolve_flag, resolver_installer_options, + resolver_options, + }, }; use uv_client::Connectivity; use uv_configuration::{ @@ -87,7 +90,7 @@ impl GlobalSettings { environment: &EnvironmentOptions, ) -> Self { let network_settings = NetworkSettings::resolve(args, workspace, environment); - let python_preference = resolve_python_preference(args, workspace); + let python_preference = resolve_python_preference(args, workspace, environment); Self { required_version: workspace .and_then(|workspace| workspace.globals.required_version.clone()), @@ -140,9 +143,7 @@ impl GlobalSettings { }, show_settings: args.show_settings, preview: Preview::from_args( - flag(args.preview, args.no_preview, "preview") - .combine(workspace.and_then(|workspace| workspace.globals.preview)) - .unwrap_or(false), + resolve_preview(args, workspace, environment), args.no_preview, &args.preview_features, ), @@ -158,8 +159,15 @@ impl GlobalSettings { .unwrap_or_default(), // Disable the progress bar with `RUST_LOG` to avoid progress fragments interleaving // with log messages. - no_progress: args.no_progress || std::env::var_os(EnvVars::RUST_LOG).is_some(), - installer_metadata: !args.no_installer_metadata, + no_progress: resolve_flag(args.no_progress, "no-progress", environment.no_progress) + .is_enabled() + || std::env::var_os(EnvVars::RUST_LOG).is_some(), + installer_metadata: !resolve_flag( + args.no_installer_metadata, + "no-installer-metadata", + environment.no_installer_metadata, + ) + .is_enabled(), } } } @@ -167,10 +175,33 @@ impl GlobalSettings { fn resolve_python_preference( args: &GlobalArgs, workspace: Option<&FilesystemOptions>, + environment: &EnvironmentOptions, ) -> PythonPreference { - if args.managed_python { + // Resolve flags from CLI and environment variables. + let managed_python = resolve_flag( + args.managed_python, + "managed-python", + environment.managed_python, + ); + let no_managed_python = resolve_flag( + args.no_managed_python, + "no-managed-python", + environment.no_managed_python, + ); + + // Check for conflicts between managed_python and python_preference. + if managed_python.is_enabled() && args.python_preference.is_some() { + check_conflicts(managed_python, Flag::from_cli("python-preference")); + } + + // Check for conflicts between no_managed_python and python_preference. + if no_managed_python.is_enabled() && args.python_preference.is_some() { + check_conflicts(no_managed_python, Flag::from_cli("python-preference")); + } + + if managed_python.is_enabled() { PythonPreference::OnlyManaged - } else if args.no_managed_python { + } else if no_managed_python.is_enabled() { PythonPreference::OnlySystem } else { args.python_preference @@ -179,10 +210,34 @@ fn resolve_python_preference( } } +/// Resolve the preview setting from CLI, environment, and workspace config. +fn resolve_preview( + args: &GlobalArgs, + workspace: Option<&FilesystemOptions>, + environment: &EnvironmentOptions, +) -> bool { + // CLI takes precedence + match flag(args.preview, args.no_preview, "preview") { + Some(value) => value, + None => { + // Check environment variable + if environment.preview.value == Some(true) { + true + } else { + // Fall back to workspace config + workspace + .and_then(|workspace| workspace.globals.preview) + .unwrap_or(false) + } + } + } +} + /// The resolved network settings to use for any invocation of the CLI. #[derive(Debug, Clone)] pub(crate) struct NetworkSettings { pub(crate) connectivity: Connectivity, + pub(crate) offline: Flag, pub(crate) native_tls: bool, pub(crate) allow_insecure_host: Vec, pub(crate) timeout: Duration, @@ -195,17 +250,45 @@ impl NetworkSettings { workspace: Option<&FilesystemOptions>, environment: &EnvironmentOptions, ) -> Self { - let connectivity = if flag(args.offline, args.no_offline, "offline") - .combine(workspace.and_then(|workspace| workspace.globals.offline)) - .unwrap_or(false) - { + // Resolve offline flag from CLI, environment variable, and workspace config. + // Precedence: CLI > Env var > Workspace config > default (false). + let offline = match flag(args.offline, args.no_offline, "offline") { + Some(true) => Flag::from_cli("offline"), + Some(false) => Flag::disabled(), + None => { + // CLI didn't provide a value, check environment variable. + let env_flag = resolve_flag(false, "offline", environment.offline); + if env_flag.is_enabled() { + env_flag + } else if workspace + .and_then(|workspace| workspace.globals.offline) + .unwrap_or(false) + { + // Workspace config enabled offline mode. + Flag::from_config("offline") + } else { + Flag::disabled() + } + } + }; + + let connectivity = if offline.is_enabled() { Connectivity::Offline } else { Connectivity::Online }; - let native_tls = flag(args.native_tls, args.no_native_tls, "native-tls") - .combine(workspace.and_then(|workspace| workspace.globals.native_tls)) - .unwrap_or(false); + let native_tls = match flag(args.native_tls, args.no_native_tls, "native-tls") { + Some(value) => value, + None => { + if environment.native_tls.value == Some(true) { + true + } else { + workspace + .and_then(|workspace| workspace.globals.native_tls) + .unwrap_or(false) + } + } + }; let allow_insecure_host = args .allow_insecure_host .as_ref() @@ -225,12 +308,26 @@ impl NetworkSettings { .collect(); Self { connectivity, + offline, native_tls, allow_insecure_host, timeout: environment.http_timeout, retries: environment.http_retries, } } + + /// Check if offline mode conflicts with a refresh request. + /// + /// This should be called when a command uses refresh functionality to ensure + /// offline mode and refresh are not both enabled. + pub(crate) fn check_refresh_conflict(&self, refresh: &Refresh) { + if !matches!(refresh, Refresh::None(_)) { + // TODO(charlie): `Refresh` isn't a `Flag`, so we create a synthetic one here + // (which matches Clap's representation). Consider a dedicated helper for + // conflicts with CLI-only arguments. + check_conflicts(self.offline, Flag::from_cli("refresh")); + } + } } /// The resolved cache settings to use for any invocation of the CLI. @@ -353,7 +450,11 @@ impl InitSettings { #[derive(Debug, Clone, Copy)] pub(crate) enum LockCheckSource { /// The user invoked `uv --locked` - Locked, + LockedCli, + /// The `UV_LOCKED` environment variable was set. + LockedEnv, + /// The `locked` option was set via workspace configuration. + LockedConfiguration, /// The user invoked `uv --check` Check, } @@ -361,7 +462,9 @@ pub(crate) enum LockCheckSource { impl std::fmt::Display for LockCheckSource { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Locked => write!(f, "--locked"), + Self::LockedCli => write!(f, "--locked"), + Self::LockedEnv => write!(f, "UV_LOCKED=1"), + Self::LockedConfiguration => write!(f, "locked (workspace configuration)"), Self::Check => write!(f, "--check"), } } @@ -376,11 +479,48 @@ pub(crate) enum LockCheck { Disabled, } +/// The source of the frozen flag. +#[derive(Debug, Clone, Copy)] +pub(crate) enum FrozenSource { + /// The `--frozen` flag was provided on CLI. + Cli, + /// The `UV_FROZEN` environment variable was set. + Env, + /// The `frozen` option was set via workspace configuration. + Configuration, +} + +/// Convert a resolved flag to an optional frozen source. +fn resolve_frozen(flag: Flag) -> Option { + if flag.is_enabled() { + Some(match flag.source() { + Some(FlagSource::Cli) | None => FrozenSource::Cli, + Some(FlagSource::Env(_)) => FrozenSource::Env, + Some(FlagSource::Config) => FrozenSource::Configuration, + }) + } else { + None + } +} + +/// Convert a resolved flag to a lock check. +fn resolve_lock_check(flag: Flag) -> LockCheck { + if flag.is_enabled() { + LockCheck::Enabled(match flag.source() { + Some(FlagSource::Cli) | None => LockCheckSource::LockedCli, + Some(FlagSource::Env(_)) => LockCheckSource::LockedEnv, + Some(FlagSource::Config) => LockCheckSource::LockedConfiguration, + }) + } else { + LockCheck::Disabled + } +} + /// The resolved settings to use for a `run` invocation. #[derive(Debug, Clone)] pub(crate) struct RunSettings { pub(crate) lock_check: LockCheck, - pub(crate) frozen: bool, + pub(crate) frozen: Option, pub(crate) extras: ExtrasSpecification, pub(crate) groups: DependencyGroups, pub(crate) editable: Option, @@ -467,13 +607,24 @@ impl RunSettings { .map(|fs| fs.install_mirrors.clone()) .unwrap_or_default(); + // Resolve flags from CLI and environment variables. + let locked = resolve_flag(locked, "locked", environment.locked); + let frozen = resolve_flag(frozen, "frozen", environment.frozen); + + // Check for conflicts between locked and frozen. + check_conflicts(locked, frozen); + + let dev = dev || environment.dev.value == Some(true); + let no_dev = no_dev || environment.no_dev.value == Some(true); + + let no_editable = no_editable || environment.no_editable.value == Some(true); + let isolated = isolated || environment.isolated.value == Some(true); + let show_resolution = show_resolution || environment.show_resolution.value == Some(true); + let no_env_file = no_env_file || environment.no_env_file.value == Some(true); + Self { - lock_check: if locked { - LockCheck::Enabled(LockCheckSource::Locked) - } else { - LockCheck::Disabled - }, - frozen, + lock_check: resolve_lock_check(locked), + frozen: resolve_frozen(frozen), extras: ExtrasSpecification::from_args( extra.unwrap_or_default(), no_extra, @@ -636,6 +787,11 @@ impl ToolRunSettings { } let lfs = GitLfsSetting::new(lfs.then_some(true), environment.lfs); + // Resolve flags from CLI and environment variables. + let isolated = isolated || environment.isolated.value == Some(true); + let show_resolution = show_resolution || environment.show_resolution.value == Some(true); + let no_env_file = no_env_file || environment.no_env_file.value == Some(true); + Self { command, from, @@ -1377,7 +1533,7 @@ impl PythonPinSettings { #[derive(Debug, Clone)] pub(crate) struct SyncSettings { pub(crate) lock_check: LockCheck, - pub(crate) frozen: bool, + pub(crate) frozen: Option, pub(crate) dry_run: DryRun, pub(crate) script: Option, pub(crate) active: Option, @@ -1462,16 +1618,22 @@ impl SyncSettings { } else { DryRun::from_args(dry_run) }; - let lock_check = if locked { - LockCheck::Enabled(LockCheckSource::Locked) - } else { - LockCheck::Disabled - }; + + // Resolve flags from CLI and environment variables. + let locked = resolve_flag(locked, "locked", environment.locked); + let frozen = resolve_flag(frozen, "frozen", environment.frozen); + + // Check for conflicts between locked and frozen. + check_conflicts(locked, frozen); + + let dev = dev || environment.dev.value == Some(true); + let no_dev = no_dev || environment.no_dev.value == Some(true); + let no_editable = no_editable || environment.no_editable.value == Some(true); Self { output_format, - lock_check, - frozen, + lock_check: resolve_lock_check(locked), + frozen: resolve_frozen(frozen), dry_run, script, active: flag(active, no_active, "active"), @@ -1528,7 +1690,7 @@ impl SyncSettings { #[derive(Debug, Clone)] pub(crate) struct LockSettings { pub(crate) lock_check: LockCheck, - pub(crate) frozen: bool, + pub(crate) frozen: Option, pub(crate) dry_run: DryRun, pub(crate) script: Option, pub(crate) python: Option, @@ -1562,17 +1724,22 @@ impl LockSettings { .map(|fs| fs.install_mirrors.clone()) .unwrap_or_default(); + // Resolve flags from CLI and environment variables. + let locked = resolve_flag(locked, "locked", environment.locked); + let frozen = resolve_flag(check_exists, "frozen", environment.frozen); + + // Check for conflicts between locked and frozen. + check_conflicts(locked, frozen); + let lock_check = if check { LockCheck::Enabled(LockCheckSource::Check) - } else if locked { - LockCheck::Enabled(LockCheckSource::Locked) } else { - LockCheck::Disabled + resolve_lock_check(locked) }; Self { lock_check, - frozen: check_exists, + frozen: resolve_frozen(frozen), dry_run: DryRun::from_args(dry_run), script, python: python.and_then(Maybe::into_option), @@ -1590,7 +1757,7 @@ impl LockSettings { #[derive(Debug, Clone)] pub(crate) struct AddSettings { pub(crate) lock_check: LockCheck, - pub(crate) frozen: bool, + pub(crate) frozen: Option, pub(crate) active: Option, pub(crate) no_sync: bool, pub(crate) packages: Vec, @@ -1672,6 +1839,10 @@ impl AddSettings { only_install_package, } = args; + // Resolve flags from CLI and environment variables. + let dev = dev || environment.dev.value == Some(true); + let no_editable = no_editable || environment.no_editable.value == Some(true); + let dependency_type = if let Some(extra) = optional { DependencyType::Optional(extra) } else if let Some(group) = group { @@ -1750,15 +1921,22 @@ impl AddSettings { let bounds = bounds.or(filesystem.as_ref().and_then(|fs| fs.add.add_bounds)); let lfs = GitLfsSetting::new(lfs.then_some(true), environment.lfs); + // Resolve flags from CLI and environment variables. + let locked = resolve_flag(locked, "locked", environment.locked); + let frozen = resolve_flag(frozen, "frozen", environment.frozen); + let no_sync = resolve_flag(no_sync, "no-sync", environment.no_sync); + + // Check for conflicts between locked and frozen. + check_conflicts(locked, frozen); + + // Check for conflicts between no_sync and frozen. + check_conflicts(no_sync, frozen); + Self { - lock_check: if locked { - LockCheck::Enabled(LockCheckSource::Locked) - } else { - LockCheck::Disabled - }, - frozen, + lock_check: resolve_lock_check(locked), + frozen: resolve_frozen(frozen), active: flag(active, no_active, "active"), - no_sync, + no_sync: no_sync.is_enabled(), packages, requirements, constraints: constraints @@ -1805,7 +1983,7 @@ impl AddSettings { #[derive(Debug, Clone)] pub(crate) struct RemoveSettings { pub(crate) lock_check: LockCheck, - pub(crate) frozen: bool, + pub(crate) frozen: Option, pub(crate) active: Option, pub(crate) no_sync: bool, pub(crate) packages: Vec, @@ -1844,6 +2022,9 @@ impl RemoveSettings { python, } = args; + // Resolve flags from CLI and environment variables. + let dev = dev || environment.dev.value == Some(true); + let dependency_type = if let Some(extra) = optional { DependencyType::Optional(extra) } else if let Some(group) = group { @@ -1864,15 +2045,22 @@ impl RemoveSettings { .map(|requirement| requirement.name) .collect(); + // Resolve flags from CLI and environment variables. + let locked = resolve_flag(locked, "locked", environment.locked); + let frozen = resolve_flag(frozen, "frozen", environment.frozen); + let no_sync = resolve_flag(no_sync, "no-sync", environment.no_sync); + + // Check for conflicts between locked and frozen. + check_conflicts(locked, frozen); + + // Check for conflicts between no_sync and frozen. + check_conflicts(no_sync, frozen); + Self { - lock_check: if locked { - LockCheck::Enabled(LockCheckSource::Locked) - } else { - LockCheck::Disabled - }, - frozen, + lock_check: resolve_lock_check(locked), + frozen: resolve_frozen(frozen), active: flag(active, no_active, "active"), - no_sync, + no_sync: no_sync.is_enabled(), packages, dependency_type, package, @@ -1900,7 +2088,7 @@ pub(crate) struct VersionSettings { pub(crate) output_format: VersionFormat, pub(crate) dry_run: bool, pub(crate) lock_check: LockCheck, - pub(crate) frozen: bool, + pub(crate) frozen: Option, pub(crate) active: Option, pub(crate) no_sync: bool, pub(crate) package: Option, @@ -1941,20 +2129,27 @@ impl VersionSettings { .map(|fs| fs.install_mirrors.clone()) .unwrap_or_default(); + // Resolve flags from CLI and environment variables. + let locked = resolve_flag(locked, "locked", environment.locked); + let frozen = resolve_flag(frozen, "frozen", environment.frozen); + let no_sync = resolve_flag(no_sync, "no-sync", environment.no_sync); + + // Check for conflicts between locked and frozen. + check_conflicts(locked, frozen); + + // Check for conflicts between no_sync and frozen. + check_conflicts(no_sync, frozen); + Self { value, bump, short, output_format, dry_run, - lock_check: if locked { - LockCheck::Enabled(LockCheckSource::Locked) - } else { - LockCheck::Disabled - }, - frozen, + lock_check: resolve_lock_check(locked), + frozen: resolve_frozen(frozen), active: flag(active, no_active, "active"), - no_sync, + no_sync: no_sync.is_enabled(), package, python: python.and_then(Maybe::into_option), refresh: Refresh::from(refresh), @@ -1974,7 +2169,7 @@ impl VersionSettings { pub(crate) struct TreeSettings { pub(crate) groups: DependencyGroups, pub(crate) lock_check: LockCheck, - pub(crate) frozen: bool, + pub(crate) frozen: Option, pub(crate) universal: bool, pub(crate) depth: u8, pub(crate) prune: Vec, @@ -2025,6 +2220,16 @@ impl TreeSettings { .map(|fs| fs.install_mirrors.clone()) .unwrap_or_default(); + // Resolve flags from CLI and environment variables. + let locked = resolve_flag(locked, "locked", environment.locked); + let frozen = resolve_flag(frozen, "frozen", environment.frozen); + + // Check for conflicts between locked and frozen. + check_conflicts(locked, frozen); + + let dev = dev || environment.dev.value == Some(true); + let no_dev = no_dev || environment.no_dev.value == Some(true); + Self { groups: DependencyGroups::from_args( dev, @@ -2036,12 +2241,8 @@ impl TreeSettings { only_group, all_groups, ), - lock_check: if locked { - LockCheck::Enabled(LockCheckSource::Locked) - } else { - LockCheck::Disabled - }, - frozen, + lock_check: resolve_lock_check(locked), + frozen: resolve_frozen(frozen), universal, depth: tree.depth, prune: tree.prune, @@ -2077,7 +2278,7 @@ pub(crate) struct ExportSettings { pub(crate) install_options: InstallOptions, pub(crate) output_file: Option, pub(crate) lock_check: LockCheck, - pub(crate) frozen: bool, + pub(crate) frozen: Option, pub(crate) include_annotations: bool, pub(crate) include_header: bool, pub(crate) script: Option, @@ -2130,7 +2331,7 @@ impl ExportSettings { no_emit_package, only_emit_package, locked, - frozen, + frozen: frozen_cli, resolver, build, refresh, @@ -2142,6 +2343,17 @@ impl ExportSettings { .map(|fs| fs.install_mirrors.clone()) .unwrap_or_default(); + // Resolve flags from CLI and environment variables. + let locked = resolve_flag(locked, "locked", environment.locked); + let frozen = resolve_flag(frozen_cli, "frozen", environment.frozen); + + // Check for conflicts between locked and frozen. + check_conflicts(locked, frozen); + + let dev = dev || environment.dev.value == Some(true); + let no_dev = no_dev || environment.no_dev.value == Some(true); + let no_editable = no_editable || environment.no_editable.value == Some(true); + Self { format, all_packages, @@ -2179,12 +2391,8 @@ impl ExportSettings { only_emit_package, ), output_file, - lock_check: if locked { - LockCheck::Enabled(LockCheckSource::Locked) - } else { - LockCheck::Disabled - }, - frozen, + lock_check: resolve_lock_check(locked), + frozen: resolve_frozen(frozen), include_annotations: flag(annotate, no_annotate, "annotate").unwrap_or(true), include_header: flag(header, no_header, "header").unwrap_or(true), script, @@ -2240,7 +2448,7 @@ pub(crate) struct PipCompileSettings { pub(crate) build_constraints: Vec, pub(crate) constraints_from_workspace: Vec, pub(crate) overrides_from_workspace: Vec, - pub(crate) excludes_from_workspace: Vec, + pub(crate) excludes_from_workspace: Vec, pub(crate) build_constraints_from_workspace: Vec, pub(crate) environments: SupportedEnvironments, pub(crate) refresh: Refresh, @@ -2556,7 +2764,7 @@ pub(crate) struct PipInstallSettings { pub(crate) dry_run: DryRun, pub(crate) constraints_from_workspace: Vec, pub(crate) overrides_from_workspace: Vec, - pub(crate) excludes_from_workspace: Vec, + pub(crate) excludes_from_workspace: Vec, pub(crate) build_constraints_from_workspace: Vec, pub(crate) modifications: Modifications, pub(crate) refresh: Refresh, @@ -3159,6 +3367,10 @@ impl VenvSettings { exclude_newer_package, } = args; + // Resolve flags from CLI and environment variables. + let seed = seed || environment.venv_seed.value == Some(true); + let clear = clear || environment.venv_clear.value == Some(true); + Self { seed, allow_existing, diff --git a/crates/uv/tests/it/help.rs b/crates/uv/tests/it/help.rs index c34ce485a48fb..c6e7144b091eb 100644 --- a/crates/uv/tests/it/help.rs +++ b/crates/uv/tests/it/help.rs @@ -57,8 +57,7 @@ fn help() { --color Control the use of color in output [possible values: auto, always, never] --native-tls - Whether to load TLS certificates from the platform's native certificate store [env: - UV_NATIVE_TLS=] + Whether to load TLS certificates from the platform's native store [env: UV_NATIVE_TLS=] --offline Disable network access [env: UV_OFFLINE=] --allow-insecure-host @@ -138,8 +137,7 @@ fn help_flag() { --color Control the use of color in output [possible values: auto, always, never] --native-tls - Whether to load TLS certificates from the platform's native certificate store [env: - UV_NATIVE_TLS=] + Whether to load TLS certificates from the platform's native store [env: UV_NATIVE_TLS=] --offline Disable network access [env: UV_OFFLINE=] --allow-insecure-host @@ -218,8 +216,7 @@ fn help_short_flag() { --color Control the use of color in output [possible values: auto, always, never] --native-tls - Whether to load TLS certificates from the platform's native certificate store [env: - UV_NATIVE_TLS=] + Whether to load TLS certificates from the platform's native store [env: UV_NATIVE_TLS=] --offline Disable network access [env: UV_OFFLINE=] --allow-insecure-host @@ -331,14 +328,14 @@ fn help_subcommand() { By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions. - + [env: UV_MANAGED_PYTHON=] --no-managed-python Disable use of uv-managed Python versions. Instead, uv will search for a suitable Python version on the system. - + [env: UV_NO_MANAGED_PYTHON=] --no-python-downloads @@ -369,7 +366,7 @@ fn help_subcommand() { - never: Disables colored output --native-tls - Whether to load TLS certificates from the platform's native certificate store. + Whether to load TLS certificates from the platform's native store. By default, uv loads certificates from the bundled `webpki-roots` crate. The `webpki-roots` are a reliable set of trust roots from Mozilla, and including them in uv @@ -378,14 +375,14 @@ fn help_subcommand() { However, in some cases, you may want to use the platform's native certificate store, especially if you're relying on a corporate trust root (e.g., for a mandatory proxy) that's included in your system's certificate store. - + [env: UV_NATIVE_TLS=] --offline Disable network access. When disabled, uv will only use locally cached data and locally available files. - + [env: UV_OFFLINE=] --allow-insecure-host @@ -406,7 +403,7 @@ fn help_subcommand() { Hide all progress outputs. For example, spinners or progress bars. - + [env: UV_NO_PROGRESS=] --directory @@ -455,7 +452,6 @@ fn help_subcommand() { Use `uv help python ` for more information on a specific command. - ----- stderr ----- "#); } @@ -604,14 +600,14 @@ fn help_subsubcommand() { By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions. - + [env: UV_MANAGED_PYTHON=] --no-managed-python Disable use of uv-managed Python versions. Instead, uv will search for a suitable Python version on the system. - + [env: UV_NO_MANAGED_PYTHON=] --no-python-downloads @@ -642,7 +638,7 @@ fn help_subsubcommand() { - never: Disables colored output --native-tls - Whether to load TLS certificates from the platform's native certificate store. + Whether to load TLS certificates from the platform's native store. By default, uv loads certificates from the bundled `webpki-roots` crate. The `webpki-roots` are a reliable set of trust roots from Mozilla, and including them in uv @@ -651,14 +647,14 @@ fn help_subsubcommand() { However, in some cases, you may want to use the platform's native certificate store, especially if you're relying on a corporate trust root (e.g., for a mandatory proxy) that's included in your system's certificate store. - + [env: UV_NATIVE_TLS=] --offline Disable network access. When disabled, uv will only use locally cached data and locally available files. - + [env: UV_OFFLINE=] --allow-insecure-host @@ -679,7 +675,7 @@ fn help_subsubcommand() { Hide all progress outputs. For example, spinners or progress bars. - + [env: UV_NO_PROGRESS=] --directory @@ -726,7 +722,6 @@ fn help_subsubcommand() { -h, --help Display the concise help for this command - ----- stderr ----- "#); } @@ -772,8 +767,7 @@ fn help_flag_subcommand() { --color Control the use of color in output [possible values: auto, always, never] --native-tls - Whether to load TLS certificates from the platform's native certificate store [env: - UV_NATIVE_TLS=] + Whether to load TLS certificates from the platform's native store [env: UV_NATIVE_TLS=] --offline Disable network access [env: UV_OFFLINE=] --allow-insecure-host @@ -853,8 +847,7 @@ fn help_flag_subsubcommand() { --color Control the use of color in output [possible values: auto, always, never] --native-tls - Whether to load TLS certificates from the platform's native certificate store [env: - UV_NATIVE_TLS=] + Whether to load TLS certificates from the platform's native store [env: UV_NATIVE_TLS=] --offline Disable network access [env: UV_OFFLINE=] --allow-insecure-host @@ -1015,8 +1008,7 @@ fn help_with_global_option() { --color Control the use of color in output [possible values: auto, always, never] --native-tls - Whether to load TLS certificates from the platform's native certificate store [env: - UV_NATIVE_TLS=] + Whether to load TLS certificates from the platform's native store [env: UV_NATIVE_TLS=] --offline Disable network access [env: UV_OFFLINE=] --allow-insecure-host @@ -1138,8 +1130,7 @@ fn help_with_no_pager() { --color Control the use of color in output [possible values: auto, always, never] --native-tls - Whether to load TLS certificates from the platform's native certificate store [env: - UV_NATIVE_TLS=] + Whether to load TLS certificates from the platform's native store [env: UV_NATIVE_TLS=] --offline Disable network access [env: UV_OFFLINE=] --allow-insecure-host diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index a37e73c0b892d..06c58f9e54e6b 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -12273,6 +12273,25 @@ fn conflicting_flags_clap_bug() { ); } +/// Test that `--offline` and `--refresh` conflict. +#[test] +fn offline_refresh_conflict() { + let context = TestContext::new("3.12"); + + uv_snapshot!(context.filters(), context.pip_install() + .arg("tqdm") + .arg("--offline") + .arg("--refresh"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: the argument `--offline` cannot be used with `--refresh` + " + ); +} + /// Test that shebang arguments are stripped when installing scripts #[test] #[cfg(unix)] diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index f951d754dfbbd..cf758de1af0b6 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -64,6 +64,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -267,6 +268,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -471,6 +473,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -707,6 +710,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -912,6 +916,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -1093,6 +1098,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -1323,6 +1329,7 @@ fn resolve_index_url() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -1561,6 +1568,7 @@ fn resolve_index_url() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -1857,6 +1865,7 @@ fn resolve_find_links() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -2084,6 +2093,7 @@ fn resolve_top_level() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -2270,6 +2280,7 @@ fn resolve_top_level() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -2506,6 +2517,7 @@ fn resolve_top_level() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -2765,6 +2777,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -2941,6 +2954,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -3117,6 +3131,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -3295,6 +3310,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -3492,6 +3508,7 @@ fn resolve_tool() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -3683,6 +3700,7 @@ fn resolve_poetry_toml() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -3893,6 +3911,7 @@ fn resolve_both() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -4142,6 +4161,7 @@ fn resolve_both_special_fields() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -4470,6 +4490,7 @@ fn resolve_config_file() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -4773,6 +4794,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -4952,6 +4974,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -5139,6 +5162,7 @@ fn allow_insecure_host() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [ Host { @@ -5340,6 +5364,7 @@ fn index_priority() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -5578,6 +5603,7 @@ fn index_priority() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -5822,6 +5848,7 @@ fn index_priority() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -6061,6 +6088,7 @@ fn index_priority() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -6307,6 +6335,7 @@ fn index_priority() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -6546,6 +6575,7 @@ fn index_priority() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -6798,6 +6828,7 @@ fn verify_hashes() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -6967,6 +6998,7 @@ fn verify_hashes() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -7134,6 +7166,7 @@ fn verify_hashes() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -7303,6 +7336,7 @@ fn verify_hashes() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -7470,6 +7504,7 @@ fn verify_hashes() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -7638,6 +7673,7 @@ fn verify_hashes() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -7821,6 +7857,7 @@ fn preview_features() { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -7855,7 +7892,7 @@ fn preview_features() { output_format: Text, dry_run: false, lock_check: Disabled, - frozen: false, + frozen: None, active: None, no_sync: false, package: None, @@ -7936,6 +7973,7 @@ fn preview_features() { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -7970,7 +8008,7 @@ fn preview_features() { output_format: Text, dry_run: false, lock_check: Disabled, - frozen: false, + frozen: None, active: None, no_sync: false, package: None, @@ -8051,6 +8089,7 @@ fn preview_features() { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -8085,7 +8124,7 @@ fn preview_features() { output_format: Text, dry_run: false, lock_check: Disabled, - frozen: false, + frozen: None, active: None, no_sync: false, package: None, @@ -8166,6 +8205,7 @@ fn preview_features() { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -8200,7 +8240,7 @@ fn preview_features() { output_format: Text, dry_run: false, lock_check: Disabled, - frozen: false, + frozen: None, active: None, no_sync: false, package: None, @@ -8281,6 +8321,7 @@ fn preview_features() { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -8315,7 +8356,7 @@ fn preview_features() { output_format: Text, dry_run: false, lock_check: Disabled, - frozen: false, + frozen: None, active: None, no_sync: false, package: None, @@ -8398,6 +8439,7 @@ fn preview_features() { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -8432,7 +8474,7 @@ fn preview_features() { output_format: Text, dry_run: false, lock_check: Disabled, - frozen: false, + frozen: None, active: None, no_sync: false, package: None, @@ -8534,6 +8576,7 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -8711,6 +8754,7 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -8911,6 +8955,7 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -9086,6 +9131,7 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -9255,6 +9301,7 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -9425,6 +9472,7 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -9660,6 +9708,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -9689,7 +9738,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { } LockSettings { lock_check: Disabled, - frozen: false, + frozen: None, dry_run: Disabled, script: None, python: None, @@ -9780,6 +9829,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -9809,7 +9859,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { } LockSettings { lock_check: Disabled, - frozen: false, + frozen: None, dry_run: Disabled, script: None, python: None, @@ -9923,6 +9973,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -9952,7 +10003,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { } LockSettings { lock_check: Disabled, - frozen: false, + frozen: None, dry_run: Disabled, script: None, python: None, @@ -10041,6 +10092,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -10070,7 +10122,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { } LockSettings { lock_check: Disabled, - frozen: false, + frozen: None, dry_run: Disabled, script: None, python: None, @@ -10149,6 +10201,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -10178,7 +10231,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { } LockSettings { lock_check: Disabled, - frozen: false, + frozen: None, dry_run: Disabled, script: None, python: None, @@ -10258,6 +10311,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -10287,7 +10341,7 @@ fn upgrade_project_cli_config_interaction() -> anyhow::Result<()> { } LockSettings { lock_check: Disabled, - frozen: false, + frozen: None, dry_run: Disabled, script: None, python: None, @@ -10431,6 +10485,7 @@ fn build_isolation_override() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME], @@ -10603,6 +10658,7 @@ fn build_isolation_override() -> anyhow::Result<()> { color: Auto, network_settings: NetworkSettings { connectivity: Online, + offline: Disabled, native_tls: false, allow_insecure_host: [], timeout: [TIME],