From e5d4ea55ca1c0cbd769810c08ade86c0ccab6333 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 17 Apr 2024 13:03:29 -0400 Subject: [PATCH] Merge workspace settings with CLI settings (#3045) ## Summary This PR adds the structs and logic necessary to respect settings from the workspace. It's a ton of code, but it's mostly mechanical. And, believe it or not, I pulled out a few refactors in advance to trim down the code and complexity. The highlights are: - All CLI arguments are now `Option`, so that we can detect whether they were provided (i.e., we can't let Clap fill in the defaults). - We now have a `*Settings` struct for each command, which merges the CLI and workspace options (e.g., `PipCompileSettings`). I've only implemented `PipCompileSettings` for now. If approved, I'll implement the others prior to merging, but it's very mechanical and I both didn't want to do the conversion prior to receiving feedback _and_ realized it would make the PR harder to review. --- crates/uv-cache/src/cli.rs | 38 +- crates/uv-workspace/src/settings.rs | 22 +- crates/uv-workspace/src/workspace.rs | 4 +- crates/uv/src/cli.rs | 182 +++--- crates/uv/src/commands/pip_compile.rs | 2 +- crates/uv/src/main.rs | 317 +++++----- crates/uv/src/settings.rs | 840 ++++++++++++++++++++++++++ 7 files changed, 1160 insertions(+), 245 deletions(-) create mode 100644 crates/uv/src/settings.rs diff --git a/crates/uv-cache/src/cli.rs b/crates/uv-cache/src/cli.rs index 18d04ab58..19780139e 100644 --- a/crates/uv-cache/src/cli.rs +++ b/crates/uv-cache/src/cli.rs @@ -16,7 +16,7 @@ pub struct CacheArgs { alias = "no-cache-dir", env = "UV_NO_CACHE" )] - no_cache: bool, + pub no_cache: Option, /// Path to the cache directory. /// @@ -24,7 +24,31 @@ pub struct CacheArgs { /// Linux, and `$HOME/.cache/ {FOLDERID_LocalAppData}//cache/uv` /// on Windows. #[arg(global = true, long, env = "UV_CACHE_DIR")] - cache_dir: Option, + pub cache_dir: Option, +} + +impl Cache { + /// Prefer, in order: + /// 1. A temporary cache directory, if the user requested `--no-cache`. + /// 2. The specific cache directory specified by the user via `--cache-dir` or `UV_CACHE_DIR`. + /// 3. The system-appropriate cache directory. + /// 4. A `.uv_cache` directory in the current working directory. + /// + /// Returns an absolute cache dir. + pub fn from_settings( + no_cache: Option, + cache_dir: Option, + ) -> Result { + if no_cache.unwrap_or(false) { + Cache::temp() + } else if let Some(cache_dir) = cache_dir { + Cache::from_path(cache_dir) + } else if let Some(project_dirs) = ProjectDirs::from("", "", "uv") { + Cache::from_path(project_dirs.cache_dir()) + } else { + Cache::from_path(".uv_cache") + } + } } impl TryFrom for Cache { @@ -38,14 +62,6 @@ impl TryFrom for Cache { /// /// Returns an absolute cache dir. fn try_from(value: CacheArgs) -> Result { - if value.no_cache { - Self::temp() - } else if let Some(cache_dir) = value.cache_dir { - Self::from_path(cache_dir) - } else if let Some(project_dirs) = ProjectDirs::from("", "", "uv") { - Self::from_path(project_dirs.cache_dir()) - } else { - Self::from_path(".uv_cache") - } + Cache::from_settings(value.no_cache, value.cache_dir) } } diff --git a/crates/uv-workspace/src/settings.rs b/crates/uv-workspace/src/settings.rs index 7d662ca56..1e7e34960 100644 --- a/crates/uv-workspace/src/settings.rs +++ b/crates/uv-workspace/src/settings.rs @@ -5,7 +5,7 @@ use serde::Deserialize; use distribution_types::{FlatIndexLocation, IndexUrl}; use install_wheel_rs::linker::LinkMode; use uv_configuration::{ConfigSettings, IndexStrategy, KeyringProviderType, PackageNameSpecifier}; -use uv_normalize::PackageName; +use uv_normalize::{ExtraName, PackageName}; use uv_resolver::{AnnotationStyle, ExcludeNewer, PreReleaseMode, ResolutionMode}; use uv_toolchain::PythonVersion; @@ -28,10 +28,8 @@ pub(crate) struct Tools { #[derive(Debug, Clone, Default, Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] pub struct Options { - pub quiet: Option, - pub verbose: Option, pub native_tls: Option, - pub no_cache: bool, + pub no_cache: Option, pub cache_dir: Option, pub pip: Option, } @@ -41,10 +39,12 @@ pub struct Options { #[derive(Debug, Clone, Default, Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] pub struct PipOptions { + pub python: Option, pub system: Option, + pub break_system_packages: Option, pub offline: Option, pub index_url: Option, - pub extra_index_url: Option, + pub extra_index_url: Option>, pub no_index: Option, pub find_links: Option>, pub index_strategy: Option, @@ -53,21 +53,29 @@ pub struct PipOptions { pub no_binary: Option>, pub only_binary: Option>, pub no_build_isolation: Option, + pub strict: Option, + pub extra: Option>, + pub all_extras: Option, + pub no_deps: Option, pub resolution: Option, pub prerelease: Option, + pub output_file: Option, pub no_strip_extras: Option, pub no_annotate: Option, pub no_header: Option, + pub custom_compile_command: Option, pub generate_hashes: Option, pub legacy_setup_py: Option, - pub config_setting: Option, + pub config_settings: Option, pub python_version: Option, pub exclude_newer: Option, pub no_emit_package: Option>, pub emit_index_url: Option, pub emit_find_links: Option, + pub emit_marker_expression: Option, + pub emit_index_annotation: Option, pub annotation_style: Option, - pub require_hashes: Option, pub link_mode: Option, pub compile_bytecode: Option, + pub require_hashes: Option, } diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index 80ef93710..daac9562f 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -10,8 +10,8 @@ use crate::{Options, PyProjectToml}; #[allow(dead_code)] #[derive(Debug, Clone)] pub struct Workspace { - options: Options, - root: PathBuf, + pub options: Options, + pub root: PathBuf, } impl Workspace { diff --git a/crates/uv/src/cli.rs b/crates/uv/src/cli.rs index d238826c9..86176b759 100644 --- a/crates/uv/src/cli.rs +++ b/crates/uv/src/cli.rs @@ -248,7 +248,7 @@ pub(crate) struct PipCompileArgs { /// Include optional dependencies in the given extra group name; may be provided more than once. #[arg(long, conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)] - pub(crate) extra: Vec, + pub(crate) extra: Option>, /// Include all optional dependencies. #[arg(long, conflicts_with = "extra")] @@ -259,11 +259,20 @@ pub(crate) struct PipCompileArgs { #[arg(long)] pub(crate) no_deps: bool, - #[arg(long, value_enum, default_value_t = ResolutionMode::default(), env = "UV_RESOLUTION")] - pub(crate) resolution: ResolutionMode, + /// The strategy to use when selecting between the different compatible versions for a given + /// package requirement. + /// + /// By default, `uv` will use the latest compatible version of each package (`highest`). + #[arg(long, value_enum, env = "UV_RESOLUTION")] + pub(crate) resolution: Option, - #[arg(long, value_enum, default_value_t = PreReleaseMode::default(), env = "UV_PRERELEASE")] - pub(crate) prerelease: PreReleaseMode, + /// The strategy to use when considering pre-release versions. + /// + /// By default, `uv` will accept pre-releases for packages that _only_ publish pre-releases, + /// along with first-party requirements that contain an explicit pre-release marker in the + /// declared specifiers (`if-necessary-or-explicit`). + #[arg(long, value_enum, env = "UV_PRERELEASE")] + pub(crate) prerelease: Option, #[arg(long, hide = true)] pub(crate) pre: bool, @@ -289,8 +298,10 @@ pub(crate) struct PipCompileArgs { pub(crate) no_header: bool, /// Choose the style of the annotation comments, which indicate the source of each package. - #[arg(long, default_value_t=AnnotationStyle::Split, value_enum)] - pub(crate) annotation_style: AnnotationStyle, + /// + /// Defaults to `split`. + #[arg(long, value_enum)] + pub(crate) annotation_style: Option, /// Change header comment to reflect custom command wrapping `uv pip compile`. #[arg(long, env = "UV_CUSTOM_COMPILE_COMMAND")] @@ -311,7 +322,7 @@ pub(crate) struct PipCompileArgs { /// Refresh cached data for a specific package. #[arg(long)] - pub(crate) refresh_package: Vec, + pub(crate) refresh_package: Option>, /// The method to use when installing packages from the global cache. /// @@ -319,8 +330,8 @@ pub(crate) struct PipCompileArgs { /// /// Defaults to `clone` (also known as Copy-on-Write) on macOS, and `hardlink` on Linux and /// Windows. - #[arg(long, value_enum, default_value_t = install_wheel_rs::linker::LinkMode::default())] - pub(crate) link_mode: install_wheel_rs::linker::LinkMode, + #[arg(long, value_enum)] + pub(crate) link_mode: Option, /// The URL of the Python package index (by default: ). /// @@ -343,7 +354,7 @@ pub(crate) struct PipCompileArgs { /// as it finds it in an index. That is, it isn't possible for `uv` to /// consider versions of the same package across multiple indexes. #[arg(long, env = "UV_EXTRA_INDEX_URL", value_delimiter = ' ', value_parser = parse_index_url)] - pub(crate) extra_index_url: Vec>, + pub(crate) extra_index_url: Option>>, /// Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those /// discovered via `--find-links`. @@ -353,18 +364,20 @@ pub(crate) struct PipCompileArgs { /// The strategy to use when resolving against multiple index URLs. /// /// By default, `uv` will stop at the first index on which a given package is available, and - /// limit resolutions to those present on that first index. This prevents "dependency confusion" - /// attacks, whereby an attack can upload a malicious package under the same name to a secondary - /// index. - #[arg(long, default_value_t, value_enum, env = "UV_INDEX_STRATEGY")] - pub(crate) index_strategy: IndexStrategy, + /// limit resolutions to those present on that first index (`first-match`. This prevents + /// "dependency confusion" attacks, whereby an attack can upload a malicious package under the + /// same name to a secondary + #[arg(long, value_enum, env = "UV_INDEX_STRATEGY")] + pub(crate) index_strategy: Option, - /// Attempt to use `keyring` for authentication for index urls + /// Attempt to use `keyring` for authentication for index URLs. /// /// Due to not having Python imports, only `--keyring-provider subprocess` argument is currently /// implemented `uv` will try to use `keyring` via CLI when this flag is used. - #[arg(long, default_value_t, value_enum, env = "UV_KEYRING_PROVIDER")] - pub(crate) keyring_provider: KeyringProviderType, + /// + /// Defaults to `disabled`. + #[arg(long, value_enum, env = "UV_KEYRING_PROVIDER")] + pub(crate) keyring_provider: Option, /// Locations to search for candidate distributions, beyond those found in the indexes. /// @@ -373,7 +386,7 @@ pub(crate) struct PipCompileArgs { /// /// If a URL, the page must contain a flat list of links to package files. #[arg(long, short)] - pub(crate) find_links: Vec, + pub(crate) find_links: Option>, /// Allow package upgrades, ignoring pinned versions in the existing output file. #[arg(long, short = 'U')] @@ -382,7 +395,7 @@ pub(crate) struct PipCompileArgs { /// Allow upgrades for a specific package, ignoring pinned versions in the existing output /// file. #[arg(long, short = 'P')] - pub(crate) upgrade_package: Vec, + pub(crate) upgrade_package: Option>, /// Include distribution hashes in the output file. #[arg(long)] @@ -418,11 +431,11 @@ pub(crate) struct PipCompileArgs { /// Multiple packages may be provided. Disable binaries for all packages with `:all:`. /// Clear previously specified packages with `:none:`. #[arg(long, conflicts_with = "no_build")] - pub(crate) only_binary: Vec, + pub(crate) only_binary: Option>, /// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs. #[arg(long, short = 'C', alias = "config-settings")] - pub(crate) config_setting: Vec, + pub(crate) config_setting: Option>, /// The minimum Python version that should be supported by the compiled requirements (e.g., /// `3.7` or `3.7.9`). @@ -442,7 +455,7 @@ pub(crate) struct PipCompileArgs { /// Specify a package to omit from the output resolution. Its dependencies will still be /// included in the resolution. Equivalent to pip-compile's `--unsafe-package` option. #[arg(long, alias = "unsafe-package")] - pub(crate) no_emit_package: Vec, + pub(crate) no_emit_package: Option>, /// Include `--index-url` and `--extra-index-url` entries in the generated output file. #[arg(long)] @@ -506,8 +519,8 @@ pub(crate) struct PipSyncArgs { /// /// Defaults to `clone` (also known as Copy-on-Write) on macOS, and `hardlink` on Linux and /// Windows. - #[arg(long, value_enum, default_value_t = install_wheel_rs::linker::LinkMode::default())] - pub(crate) link_mode: install_wheel_rs::linker::LinkMode, + #[arg(long, value_enum)] + pub(crate) link_mode: Option, /// The URL of the Python package index (by default: ). /// @@ -530,7 +543,7 @@ pub(crate) struct PipSyncArgs { /// as it finds it in an index. That is, it isn't possible for `uv` to /// consider versions of the same package across multiple indexes. #[arg(long, env = "UV_EXTRA_INDEX_URL", value_delimiter = ' ', value_parser = parse_index_url)] - pub(crate) extra_index_url: Vec>, + pub(crate) extra_index_url: Option>>, /// Locations to search for candidate distributions, beyond those found in the indexes. /// @@ -539,7 +552,7 @@ pub(crate) struct PipSyncArgs { /// /// If a URL, the page must contain a flat list of links to package files. #[arg(long, short)] - pub(crate) find_links: Vec, + pub(crate) find_links: Option>, /// Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those /// discovered via `--find-links`. @@ -549,11 +562,11 @@ pub(crate) struct PipSyncArgs { /// The strategy to use when resolving against multiple index URLs. /// /// By default, `uv` will stop at the first index on which a given package is available, and - /// limit resolutions to those present on that first index. This prevents "dependency confusion" - /// attacks, whereby an attack can upload a malicious package under the same name to a secondary - /// index. - #[arg(long, default_value_t, value_enum, env = "UV_INDEX_STRATEGY")] - pub(crate) index_strategy: IndexStrategy, + /// limit resolutions to those present on that first index (`first-match`. This prevents + /// "dependency confusion" attacks, whereby an attack can upload a malicious package under the + /// same name to a secondary + #[arg(long, value_enum, env = "UV_INDEX_STRATEGY")] + pub(crate) index_strategy: Option, /// Require a matching hash for each requirement. /// @@ -569,12 +582,14 @@ pub(crate) struct PipSyncArgs { #[arg(long)] pub(crate) require_hashes: bool, - /// Attempt to use `keyring` for authentication for index urls + /// Attempt to use `keyring` for authentication for index URLs. /// /// Function's similar to `pip`'s `--keyring-provider subprocess` argument, /// `uv` will try to use `keyring` via CLI when this flag is used. - #[arg(long, default_value_t, value_enum, env = "UV_KEYRING_PROVIDER")] - pub(crate) keyring_provider: KeyringProviderType, + /// + /// Defaults to `disabled`. + #[arg(long, value_enum, env = "UV_KEYRING_PROVIDER")] + pub(crate) keyring_provider: Option, /// The Python interpreter into which packages should be installed. /// @@ -640,7 +655,7 @@ pub(crate) struct PipSyncArgs { /// Multiple packages may be provided. Disable binaries for all packages with `:all:`. /// Clear previously specified packages with `:none:`. #[arg(long, conflicts_with = "no_build")] - pub(crate) no_binary: Vec, + pub(crate) no_binary: Option>, /// Only use pre-built wheels; don't build source distributions. /// @@ -651,7 +666,7 @@ pub(crate) struct PipSyncArgs { /// Multiple packages may be provided. Disable binaries for all packages with `:all:`. /// Clear previously specified packages with `:none:`. #[arg(long, conflicts_with = "no_build")] - pub(crate) only_binary: Vec, + pub(crate) only_binary: Option>, /// Compile Python files to bytecode. /// @@ -671,7 +686,7 @@ pub(crate) struct PipSyncArgs { /// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs. #[arg(long, short = 'C', alias = "config-settings")] - pub(crate) config_setting: Vec, + pub(crate) config_setting: Option>, /// Validate the virtual environment after completing the installation, to detect packages with /// missing dependencies or other issues. @@ -722,7 +737,7 @@ pub(crate) struct PipInstallArgs { /// Include optional dependencies in the given extra group name; may be provided more than once. #[arg(long, conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)] - pub(crate) extra: Vec, + pub(crate) extra: Option>, /// Include all optional dependencies. #[arg(long, conflicts_with = "extra")] @@ -734,7 +749,7 @@ pub(crate) struct PipInstallArgs { /// Allow upgrade of a specific package. #[arg(long, short = 'P')] - pub(crate) upgrade_package: Vec, + pub(crate) upgrade_package: Option>, /// Reinstall all packages, regardless of whether they're already installed. #[arg(long, alias = "force-reinstall")] @@ -742,7 +757,7 @@ pub(crate) struct PipInstallArgs { /// Reinstall a specific package, regardless of whether it's already installed. #[arg(long)] - pub(crate) reinstall_package: Vec, + pub(crate) reinstall_package: Option>, /// Run offline, i.e., without accessing the network. #[arg( @@ -759,7 +774,7 @@ pub(crate) struct PipInstallArgs { /// Refresh cached data for a specific package. #[arg(long)] - pub(crate) refresh_package: Vec, + pub(crate) refresh_package: Option>, /// Ignore package dependencies, instead only installing those packages explicitly listed /// on the command line or in the requirements files. @@ -770,14 +785,23 @@ pub(crate) struct PipInstallArgs { /// /// Defaults to `clone` (also known as Copy-on-Write) on macOS, and `hardlink` on Linux and /// Windows. - #[arg(long, value_enum, default_value_t = install_wheel_rs::linker::LinkMode::default())] - pub(crate) link_mode: install_wheel_rs::linker::LinkMode, + #[arg(long, value_enum)] + pub(crate) link_mode: Option, - #[arg(long, value_enum, default_value_t = ResolutionMode::default(), env = "UV_RESOLUTION")] - pub(crate) resolution: ResolutionMode, + /// The strategy to use when selecting between the different compatible versions for a given + /// package requirement. + /// + /// By default, `uv` will use the latest compatible version of each package (`highest`). + #[arg(long, value_enum, env = "UV_RESOLUTION")] + pub(crate) resolution: Option, - #[arg(long, value_enum, default_value_t = PreReleaseMode::default(), env = "UV_PRERELEASE")] - pub(crate) prerelease: PreReleaseMode, + /// The strategy to use when considering pre-release versions. + /// + /// By default, `uv` will accept pre-releases for packages that _only_ publish pre-releases, + /// along with first-party requirements that contain an explicit pre-release marker in the + /// declared specifiers (`if-necessary-or-explicit`). + #[arg(long, value_enum, env = "UV_PRERELEASE")] + pub(crate) prerelease: Option, #[arg(long, hide = true)] pub(crate) pre: bool, @@ -803,7 +827,7 @@ pub(crate) struct PipInstallArgs { /// as it finds it in an index. That is, it isn't possible for `uv` to /// consider versions of the same package across multiple indexes. #[arg(long, env = "UV_EXTRA_INDEX_URL", value_delimiter = ' ', value_parser = parse_index_url)] - pub(crate) extra_index_url: Vec>, + pub(crate) extra_index_url: Option>>, /// Locations to search for candidate distributions, beyond those found in the indexes. /// @@ -812,7 +836,7 @@ pub(crate) struct PipInstallArgs { /// /// If a URL, the page must contain a flat list of links to package files. #[arg(long, short)] - pub(crate) find_links: Vec, + pub(crate) find_links: Option>, /// Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those /// discovered via `--find-links`. @@ -822,11 +846,11 @@ pub(crate) struct PipInstallArgs { /// The strategy to use when resolving against multiple index URLs. /// /// By default, `uv` will stop at the first index on which a given package is available, and - /// limit resolutions to those present on that first index. This prevents "dependency confusion" - /// attacks, whereby an attack can upload a malicious package under the same name to a secondary - /// index. - #[arg(long, default_value_t, value_enum, env = "UV_INDEX_STRATEGY")] - pub(crate) index_strategy: IndexStrategy, + /// limit resolutions to those present on that first index (`first-match`. This prevents + /// "dependency confusion" attacks, whereby an attack can upload a malicious package under the + /// same name to a secondary + #[arg(long, value_enum, env = "UV_INDEX_STRATEGY")] + pub(crate) index_strategy: Option, /// Require a matching hash for each requirement. /// @@ -842,12 +866,14 @@ pub(crate) struct PipInstallArgs { #[arg(long)] pub(crate) require_hashes: bool, - /// Attempt to use `keyring` for authentication for index urls + /// Attempt to use `keyring` for authentication for index URLs. /// /// Due to not having Python imports, only `--keyring-provider subprocess` argument is currently /// implemented `uv` will try to use `keyring` via CLI when this flag is used. - #[arg(long, default_value_t, value_enum, env = "UV_KEYRING_PROVIDER")] - pub(crate) keyring_provider: KeyringProviderType, + /// + /// Defaults to `disabled`. + #[arg(long, value_enum, env = "UV_KEYRING_PROVIDER")] + pub(crate) keyring_provider: Option, /// The Python interpreter into which packages should be installed. /// @@ -913,7 +939,7 @@ pub(crate) struct PipInstallArgs { /// Multiple packages may be provided. Disable binaries for all packages with `:all:`. /// Clear previously specified packages with `:none:`. #[arg(long, conflicts_with = "no_build")] - pub(crate) no_binary: Vec, + pub(crate) no_binary: Option>, /// Only use pre-built wheels; don't build source distributions. /// @@ -924,7 +950,7 @@ pub(crate) struct PipInstallArgs { /// Multiple packages may be provided. Disable binaries for all packages with `:all:`. /// Clear previously specified packages with `:none:`. #[arg(long, conflicts_with = "no_build")] - pub(crate) only_binary: Vec, + pub(crate) only_binary: Option>, /// Compile Python files to bytecode. /// @@ -944,7 +970,7 @@ pub(crate) struct PipInstallArgs { /// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs. #[arg(long, short = 'C', alias = "config-settings")] - pub(crate) config_setting: Vec, + pub(crate) config_setting: Option>, /// Validate the virtual environment after completing the installation, to detect packages with /// missing dependencies or other issues. @@ -995,8 +1021,10 @@ pub(crate) struct PipUninstallArgs { /// /// Due to not having Python imports, only `--keyring-provider subprocess` argument is currently /// implemented `uv` will try to use `keyring` via CLI when this flag is used. - #[arg(long, default_value_t, value_enum, env = "UV_KEYRING_PROVIDER")] - pub(crate) keyring_provider: KeyringProviderType, + /// + /// Defaults to `disabled`. + #[arg(long, value_enum, env = "UV_KEYRING_PROVIDER")] + pub(crate) keyring_provider: Option, /// Use the system Python to uninstall packages. /// @@ -1209,7 +1237,7 @@ pub(crate) struct VenvArgs { /// WARNING: `--system` is intended for use in continuous integration (CI) environments and /// should be used with caution, as it can modify the system Python installation. #[arg(long, env = "UV_SYSTEM_PYTHON", group = "discovery")] - system: bool, + pub(crate) system: bool, /// Install seed packages (`pip`, `setuptools`, and `wheel`) into the virtual environment. #[arg(long)] @@ -1247,8 +1275,8 @@ pub(crate) struct VenvArgs { /// /// Defaults to `clone` (also known as Copy-on-Write) on macOS, and `hardlink` on Linux and /// Windows. - #[arg(long, value_enum, default_value_t = install_wheel_rs::linker::LinkMode::default())] - pub(crate) link_mode: install_wheel_rs::linker::LinkMode, + #[arg(long, value_enum)] + pub(crate) link_mode: Option, /// The URL of the Python package index (by default: ). /// @@ -1271,7 +1299,7 @@ pub(crate) struct VenvArgs { /// as it finds it in an index. That is, it isn't possible for `uv` to /// consider versions of the same package across multiple indexes. #[arg(long, env = "UV_EXTRA_INDEX_URL", value_delimiter = ' ', value_parser = parse_index_url)] - pub(crate) extra_index_url: Vec>, + pub(crate) extra_index_url: Option>>, /// Ignore the registry index (e.g., PyPI), instead relying on direct URL dependencies and those /// discovered via `--find-links`. @@ -1281,18 +1309,20 @@ pub(crate) struct VenvArgs { /// The strategy to use when resolving against multiple index URLs. /// /// By default, `uv` will stop at the first index on which a given package is available, and - /// limit resolutions to those present on that first index. This prevents "dependency confusion" - /// attacks, whereby an attack can upload a malicious package under the same name to a secondary - /// index. - #[arg(long, default_value_t, value_enum, env = "UV_INDEX_STRATEGY")] - pub(crate) index_strategy: IndexStrategy, + /// limit resolutions to those present on that first index (`first-match`. This prevents + /// "dependency confusion" attacks, whereby an attack can upload a malicious package under the + /// same name to a secondary + #[arg(long, value_enum, env = "UV_INDEX_STRATEGY")] + pub(crate) index_strategy: Option, - /// Attempt to use `keyring` for authentication for index urls + /// Attempt to use `keyring` for authentication for index URLs. /// /// Due to not having Python imports, only `--keyring-provider subprocess` argument is currently /// implemented `uv` will try to use `keyring` via CLI when this flag is used. - #[arg(long, default_value_t, value_enum, env = "UV_KEYRING_PROVIDER")] - pub(crate) keyring_provider: KeyringProviderType, + /// + /// Defaults to `disabled`. + #[arg(long, value_enum, env = "UV_KEYRING_PROVIDER")] + pub(crate) keyring_provider: Option, /// Run offline, i.e., without accessing the network. #[arg(global = true, long)] diff --git a/crates/uv/src/commands/pip_compile.rs b/crates/uv/src/commands/pip_compile.rs index b86c80fc9..8f99152a8 100644 --- a/crates/uv/src/commands/pip_compile.rs +++ b/crates/uv/src/commands/pip_compile.rs @@ -79,9 +79,9 @@ pub(crate) async fn pip_compile( python_version: Option, exclude_newer: Option, annotation_style: AnnotationStyle, + link_mode: LinkMode, native_tls: bool, quiet: bool, - link_mode: LinkMode, cache: Cache, printer: Printer, ) -> Result { diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index 885d00bcf..f327946f6 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -13,15 +13,19 @@ use tracing::instrument; use distribution_types::IndexLocations; use uv_cache::{Cache, Refresh}; use uv_client::Connectivity; -use uv_configuration::{ConfigSettings, NoBinary, NoBuild, Reinstall, SetupPyStrategy, Upgrade}; +use uv_configuration::{NoBinary, NoBuild, Reinstall, SetupPyStrategy, Upgrade}; use uv_requirements::{ExtrasSpecification, RequirementsSource}; -use uv_resolver::{DependencyMode, PreReleaseMode}; +use uv_resolver::DependencyMode; -use crate::cli::{CacheCommand, CacheNamespace, Cli, Commands, Maybe, PipCommand, PipNamespace}; +use crate::cli::{CacheCommand, CacheNamespace, Cli, Commands, PipCommand, PipNamespace}; #[cfg(feature = "self-update")] use crate::cli::{SelfCommand, SelfNamespace}; use crate::commands::ExitStatus; use crate::compat::CompatArgs; +use crate::settings::{ + CacheSettings, GlobalSettings, PipCheckSettings, PipCompileSettings, PipFreezeSettings, + PipInstallSettings, PipListSettings, PipShowSettings, PipSyncSettings, PipUninstallSettings, +}; #[cfg(target_os = "windows")] #[global_allocator] @@ -44,6 +48,7 @@ mod commands; mod compat; mod logging; mod printer; +mod settings; mod shell; mod version; @@ -104,7 +109,11 @@ async fn run() -> Result { } }; - let globals = cli.global_args; + // Load the workspace settings. + let workspace = uv_workspace::Workspace::find(env::current_dir()?)?; + + // Resolve the global settings. + let globals = GlobalSettings::resolve(cli.global_args, workspace.as_ref()); // Configure the `tracing` crate, which controls internal logging. #[cfg(feature = "tracing-durations-export")] @@ -134,11 +143,7 @@ async fn run() -> Result { uv_warnings::enable(); } - if globals.no_color { - anstream::ColorChoice::write_global(anstream::ColorChoice::Never); - } else { - anstream::ColorChoice::write_global(globals.color.into()); - } + anstream::ColorChoice::write_global(globals.color.into()); miette::set_hook(Box::new(|_| { Box::new( @@ -151,7 +156,9 @@ async fn run() -> Result { ) }))?; - let cache = Cache::try_from(cli.cache_args)?; + // Resolve the cache settings. + let cache = CacheSettings::resolve(cli.cache_args, workspace.as_ref()); + let cache = Cache::from_settings(cache.no_cache, cache.cache_dir)?; match cli.command { Commands::Pip(PipNamespace { @@ -159,6 +166,9 @@ async fn run() -> Result { }) => { args.compat_args.validate()?; + // Resolve the settings from the command-line arguments and workspace configuration. + let args = PipCompileSettings::resolve(args, workspace); + let cache = cache.with_refresh(Refresh::from_args(args.refresh, args.refresh_package)); let requirements = args .src_file @@ -176,77 +186,70 @@ async fn run() -> Result { .map(RequirementsSource::from_overrides_txt) .collect::>(); let index_urls = IndexLocations::new( - args.index_url.and_then(Maybe::into_option), - args.extra_index_url - .into_iter() - .filter_map(Maybe::into_option) - .collect(), - args.find_links, - args.no_index, + args.shared.index_url, + args.shared.extra_index_url, + args.shared.find_links, + args.shared.no_index, ); - let extras = if args.all_extras { + // TODO(charlie): Move into `PipCompileSettings::resolve`. + let extras = if args.shared.all_extras { ExtrasSpecification::All - } else if args.extra.is_empty() { + } else if args.shared.extra.is_empty() { ExtrasSpecification::None } else { - ExtrasSpecification::Some(&args.extra) + ExtrasSpecification::Some(&args.shared.extra) }; let upgrade = Upgrade::from_args(args.upgrade, args.upgrade_package); - let no_build = NoBuild::from_args(args.only_binary, args.no_build); - let dependency_mode = if args.no_deps { + let no_build = NoBuild::from_args(args.shared.only_binary, args.shared.no_build); + let dependency_mode = if args.shared.no_deps { DependencyMode::Direct } else { DependencyMode::Transitive }; - let prerelease = if args.pre { - PreReleaseMode::Allow - } else { - args.prerelease - }; - let setup_py = if args.legacy_setup_py { + let setup_py = if args.shared.legacy_setup_py { SetupPyStrategy::Setuptools } else { SetupPyStrategy::Pep517 }; - let config_settings = args.config_setting.into_iter().collect::(); + commands::pip_compile( &requirements, &constraints, &overrides, extras, - args.output_file.as_deref(), - args.resolution, - prerelease, + args.shared.output_file.as_deref(), + args.shared.resolution, + args.shared.prerelease, dependency_mode, upgrade, - args.generate_hashes, - args.no_emit_package, - args.no_strip_extras, - !args.no_annotate, - !args.no_header, - args.custom_compile_command, - args.emit_index_url, - args.emit_find_links, - args.emit_marker_expression, - args.emit_index_annotation, + args.shared.generate_hashes, + args.shared.no_emit_package, + args.shared.no_strip_extras, + !args.shared.no_annotate, + !args.shared.no_header, + args.shared.custom_compile_command, + args.shared.emit_index_url, + args.shared.emit_find_links, + args.shared.emit_marker_expression, + args.shared.emit_index_annotation, index_urls, - args.index_strategy, - args.keyring_provider, + args.shared.index_strategy, + args.shared.keyring_provider, setup_py, - config_settings, - if args.offline { + args.shared.config_setting, + if args.shared.offline { Connectivity::Offline } else { Connectivity::Online }, - args.no_build_isolation, + args.shared.no_build_isolation, no_build, - args.python_version, - args.exclude_newer, - args.annotation_style, + args.shared.python_version, + args.shared.exclude_newer, + args.shared.annotation_style, + args.shared.link_mode, globals.native_tls, globals.quiet, - args.link_mode, cache, printer, ) @@ -257,15 +260,15 @@ async fn run() -> Result { }) => { args.compat_args.validate()?; + // Resolve the settings from the command-line arguments and workspace configuration. + let args = PipSyncSettings::resolve(args, workspace); + let cache = cache.with_refresh(Refresh::from_args(args.refresh, args.refresh_package)); let index_urls = IndexLocations::new( - args.index_url.and_then(Maybe::into_option), - args.extra_index_url - .into_iter() - .filter_map(Maybe::into_option) - .collect(), - args.find_links, - args.no_index, + args.shared.index_url, + args.shared.extra_index_url, + args.shared.find_links, + args.shared.no_index, ); let sources = args .src_file @@ -273,38 +276,37 @@ async fn run() -> Result { .map(RequirementsSource::from_requirements_file) .collect::>(); let reinstall = Reinstall::from_args(args.reinstall, args.reinstall_package); - let no_binary = NoBinary::from_args(args.no_binary); - let no_build = NoBuild::from_args(args.only_binary, args.no_build); - let setup_py = if args.legacy_setup_py { + let no_binary = NoBinary::from_args(args.shared.no_binary); + let no_build = NoBuild::from_args(args.shared.only_binary, args.shared.no_build); + let setup_py = if args.shared.legacy_setup_py { SetupPyStrategy::Setuptools } else { SetupPyStrategy::Pep517 }; - let config_settings = args.config_setting.into_iter().collect::(); commands::pip_sync( &sources, &reinstall, - args.link_mode, - args.compile, - args.require_hashes, + args.shared.link_mode, + args.shared.compile_bytecode, + args.shared.require_hashes, index_urls, - args.index_strategy, - args.keyring_provider, + args.shared.index_strategy, + args.shared.keyring_provider, setup_py, - if args.offline { + if args.shared.offline { Connectivity::Offline } else { Connectivity::Online }, - &config_settings, - args.no_build_isolation, + &args.shared.config_setting, + args.shared.no_build_isolation, no_build, no_binary, - args.strict, - args.python, - args.system, - args.break_system_packages, + args.shared.strict, + args.shared.python, + args.shared.system, + args.shared.break_system_packages, globals.native_tls, cache, printer, @@ -314,6 +316,9 @@ async fn run() -> Result { Commands::Pip(PipNamespace { command: PipCommand::Install(args), }) => { + // Resolve the settings from the command-line arguments and workspace configuration. + let args = PipInstallSettings::resolve(args, workspace); + let cache = cache.with_refresh(Refresh::from_args(args.refresh, args.refresh_package)); let requirements = args .package @@ -337,73 +342,64 @@ async fn run() -> Result { .map(RequirementsSource::from_overrides_txt) .collect::>(); let index_urls = IndexLocations::new( - args.index_url.and_then(Maybe::into_option), - args.extra_index_url - .into_iter() - .filter_map(Maybe::into_option) - .collect(), - args.find_links, - args.no_index, + args.shared.index_url, + args.shared.extra_index_url, + args.shared.find_links, + args.shared.no_index, ); - let extras = if args.all_extras { + let extras = if args.shared.all_extras { ExtrasSpecification::All - } else if args.extra.is_empty() { + } else if args.shared.extra.is_empty() { ExtrasSpecification::None } else { - ExtrasSpecification::Some(&args.extra) + ExtrasSpecification::Some(&args.shared.extra) }; let reinstall = Reinstall::from_args(args.reinstall, args.reinstall_package); let upgrade = Upgrade::from_args(args.upgrade, args.upgrade_package); - let no_binary = NoBinary::from_args(args.no_binary); - let no_build = NoBuild::from_args(args.only_binary, args.no_build); - let dependency_mode = if args.no_deps { + let no_binary = NoBinary::from_args(args.shared.no_binary); + let no_build = NoBuild::from_args(args.shared.only_binary, args.shared.no_build); + let dependency_mode = if args.shared.no_deps { DependencyMode::Direct } else { DependencyMode::Transitive }; - let prerelease = if args.pre { - PreReleaseMode::Allow - } else { - args.prerelease - }; - let setup_py = if args.legacy_setup_py { + let setup_py = if args.shared.legacy_setup_py { SetupPyStrategy::Setuptools } else { SetupPyStrategy::Pep517 }; - let config_settings = args.config_setting.into_iter().collect::(); commands::pip_install( &requirements, &constraints, &overrides, &extras, - args.resolution, - prerelease, + args.shared.resolution, + args.shared.prerelease, dependency_mode, upgrade, index_urls, - args.index_strategy, - args.keyring_provider, + args.shared.index_strategy, + args.shared.keyring_provider, reinstall, - args.link_mode, - args.compile, - args.require_hashes, + args.shared.link_mode, + args.shared.compile_bytecode, + args.shared.require_hashes, setup_py, - if args.offline { + if args.shared.offline { Connectivity::Offline } else { Connectivity::Online }, - &config_settings, - args.no_build_isolation, + &args.shared.config_setting, + args.shared.no_build_isolation, no_build, no_binary, - args.strict, - args.exclude_newer, - args.python, - args.system, - args.break_system_packages, + args.shared.strict, + args.shared.exclude_newer, + args.shared.python, + args.shared.system, + args.shared.break_system_packages, globals.native_tls, cache, args.dry_run, @@ -414,6 +410,9 @@ async fn run() -> Result { Commands::Pip(PipNamespace { command: PipCommand::Uninstall(args), }) => { + // Resolve the settings from the command-line arguments and workspace configuration. + let args = PipUninstallSettings::resolve(args, workspace); + let sources = args .package .into_iter() @@ -426,61 +425,84 @@ async fn run() -> Result { .collect::>(); commands::pip_uninstall( &sources, - args.python, - args.system, - args.break_system_packages, + args.shared.python, + args.shared.system, + args.shared.break_system_packages, cache, - if args.offline { + if args.shared.offline { Connectivity::Offline } else { Connectivity::Online }, globals.native_tls, - args.keyring_provider, + args.shared.keyring_provider, printer, ) .await } Commands::Pip(PipNamespace { command: PipCommand::Freeze(args), - }) => commands::pip_freeze( - args.exclude_editable, - args.strict, - args.python.as_deref(), - args.system, - &cache, - printer, - ), + }) => { + // Resolve the settings from the command-line arguments and workspace configuration. + let args = PipFreezeSettings::resolve(args, workspace); + + commands::pip_freeze( + args.exclude_editable, + args.shared.strict, + args.shared.python.as_deref(), + args.shared.system, + &cache, + printer, + ) + } Commands::Pip(PipNamespace { command: PipCommand::List(args), }) => { args.compat_args.validate()?; + // Resolve the settings from the command-line arguments and workspace configuration. + let args = PipListSettings::resolve(args, workspace); + commands::pip_list( args.editable, args.exclude_editable, &args.exclude, &args.format, - args.strict, - args.python.as_deref(), - args.system, + args.shared.strict, + args.shared.python.as_deref(), + args.shared.system, &cache, printer, ) } Commands::Pip(PipNamespace { command: PipCommand::Show(args), - }) => commands::pip_show( - args.package, - args.strict, - args.python.as_deref(), - args.system, - &cache, - printer, - ), + }) => { + // Resolve the settings from the command-line arguments and workspace configuration. + let args = PipShowSettings::resolve(args, workspace); + + commands::pip_show( + args.package, + args.shared.strict, + args.shared.python.as_deref(), + args.shared.system, + &cache, + printer, + ) + } Commands::Pip(PipNamespace { command: PipCommand::Check(args), - }) => commands::pip_check(args.python.as_deref(), args.system, &cache, printer), + }) => { + // Resolve the settings from the command-line arguments and workspace configuration. + let args = PipCheckSettings::resolve(args, workspace); + + commands::pip_check( + args.shared.python.as_deref(), + args.shared.system, + &cache, + printer, + ) + } Commands::Cache(CacheNamespace { command: CacheCommand::Clean(args), }) @@ -497,15 +519,14 @@ async fn run() -> Result { Commands::Venv(args) => { args.compat_args.validate()?; + // Resolve the settings from the command-line arguments and workspace configuration. + let args = settings::VenvSettings::resolve(args, workspace); + let index_locations = IndexLocations::new( - args.index_url.and_then(Maybe::into_option), - args.extra_index_url - .into_iter() - .filter_map(Maybe::into_option) - .collect(), - // No find links for the venv subcommand, to keep things simple - Vec::new(), - args.no_index, + args.shared.index_url, + args.shared.extra_index_url, + args.shared.find_links, + args.shared.no_index, ); // Since we use ".venv" as the default name, we use "." as the default prompt. @@ -519,20 +540,20 @@ async fn run() -> Result { commands::venv( &args.name, - args.python.as_deref(), - args.link_mode, + args.shared.python.as_deref(), + args.shared.link_mode, &index_locations, - args.index_strategy, - args.keyring_provider, + args.shared.index_strategy, + args.shared.keyring_provider, uv_virtualenv::Prompt::from_args(prompt), args.system_site_packages, - if args.offline { + if args.shared.offline { Connectivity::Offline } else { Connectivity::Online }, args.seed, - args.exclude_newer, + args.shared.exclude_newer, globals.native_tls, &cache, printer, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs new file mode 100644 index 000000000..1ae929c8c --- /dev/null +++ b/crates/uv/src/settings.rs @@ -0,0 +1,840 @@ +use std::path::PathBuf; + +use distribution_types::{FlatIndexLocation, IndexUrl}; +use install_wheel_rs::linker::LinkMode; +use uv_cache::CacheArgs; +use uv_configuration::{ConfigSettings, IndexStrategy, KeyringProviderType, PackageNameSpecifier}; +use uv_normalize::{ExtraName, PackageName}; +use uv_resolver::{AnnotationStyle, ExcludeNewer, PreReleaseMode, ResolutionMode}; +use uv_toolchain::PythonVersion; +use uv_workspace::{PipOptions, Workspace}; + +use crate::cli::{ + ColorChoice, GlobalArgs, Maybe, PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, + PipListArgs, PipShowArgs, PipSyncArgs, PipUninstallArgs, VenvArgs, +}; +use crate::commands::ListFormat; + +/// The resolved global settings to use for any invocation of the CLI. +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone)] +pub(crate) struct GlobalSettings { + pub(crate) quiet: bool, + pub(crate) verbose: u8, + pub(crate) color: ColorChoice, + pub(crate) native_tls: bool, +} + +impl GlobalSettings { + /// Resolve the [`GlobalSettings`] from the CLI and workspace configuration. + pub(crate) fn resolve(args: GlobalArgs, workspace: Option<&Workspace>) -> Self { + Self { + quiet: args.quiet, + verbose: args.verbose, + color: if args.no_color { + ColorChoice::Never + } else { + args.color + }, + native_tls: args.native_tls + || workspace + .and_then(|workspace| workspace.options.native_tls) + .unwrap_or(false), + } + } +} + +/// The resolved cache settings to use for any invocation of the CLI. +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone)] +pub(crate) struct CacheSettings { + pub(crate) no_cache: Option, + pub(crate) cache_dir: Option, +} + +impl CacheSettings { + /// Resolve the [`CacheSettings`] from the CLI and workspace configuration. + pub(crate) fn resolve(args: CacheArgs, workspace: Option<&Workspace>) -> Self { + Self { + no_cache: args + .no_cache + .or(workspace.and_then(|workspace| workspace.options.no_cache)), + cache_dir: args + .cache_dir + .or_else(|| workspace.and_then(|workspace| workspace.options.cache_dir.clone())), + } + } +} + +/// The resolved settings to use for a `pip compile` invocation. +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone)] +pub(crate) struct PipCompileSettings { + // CLI-only settings. + pub(crate) src_file: Vec, + pub(crate) constraint: Vec, + pub(crate) r#override: Vec, + pub(crate) refresh: bool, + pub(crate) refresh_package: Vec, + pub(crate) upgrade: bool, + pub(crate) upgrade_package: Vec, + + // Shared settings. + pub(crate) shared: PipSharedSettings, +} + +impl PipCompileSettings { + /// Resolve the [`PipCompileSettings`] from the CLI and workspace configuration. + pub(crate) fn resolve(args: PipCompileArgs, workspace: Option) -> Self { + let PipCompileArgs { + src_file, + constraint, + r#override, + extra, + all_extras, + no_deps, + resolution, + prerelease, + pre, + output_file, + no_strip_extras, + no_annotate, + no_header, + annotation_style, + custom_compile_command, + offline, + refresh, + refresh_package, + link_mode, + index_url, + extra_index_url, + no_index, + index_strategy, + keyring_provider, + find_links, + upgrade, + upgrade_package, + generate_hashes, + legacy_setup_py, + no_build_isolation, + no_build, + only_binary, + config_setting, + python_version, + exclude_newer, + no_emit_package, + emit_index_url, + emit_find_links, + emit_marker_expression, + emit_index_annotation, + compat_args: _, + } = args; + + Self { + // CLI-only settings. + src_file, + constraint, + r#override, + refresh, + refresh_package: refresh_package.unwrap_or_default(), + upgrade, + upgrade_package: upgrade_package.unwrap_or_default(), + + // Shared settings. + shared: PipSharedSettings::combine( + PipOptions { + offline: Some(offline), + index_url: index_url.and_then(Maybe::into_option), + extra_index_url: extra_index_url.map(|extra_index_urls| { + extra_index_urls + .into_iter() + .filter_map(Maybe::into_option) + .collect() + }), + no_index: Some(no_index), + find_links, + index_strategy, + keyring_provider, + no_build: Some(no_build), + only_binary, + no_build_isolation: Some(no_build_isolation), + extra, + all_extras: Some(all_extras), + no_deps: Some(no_deps), + resolution, + prerelease: if pre { + Some(PreReleaseMode::Allow) + } else { + prerelease + }, + output_file, + no_strip_extras: Some(no_strip_extras), + no_annotate: Some(no_annotate), + no_header: Some(no_header), + custom_compile_command, + generate_hashes: Some(generate_hashes), + legacy_setup_py: Some(legacy_setup_py), + config_settings: config_setting.map(|config_settings| { + config_settings.into_iter().collect::() + }), + python_version, + exclude_newer, + no_emit_package, + emit_index_url: Some(emit_index_url), + emit_find_links: Some(emit_find_links), + emit_marker_expression: Some(emit_marker_expression), + emit_index_annotation: Some(emit_index_annotation), + annotation_style, + link_mode, + ..PipOptions::default() + }, + workspace, + ), + } + } +} + +/// The resolved settings to use for a `pip sync` invocation. +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone)] +pub(crate) struct PipSyncSettings { + // CLI-only settings. + pub(crate) src_file: Vec, + pub(crate) reinstall: bool, + pub(crate) reinstall_package: Vec, + pub(crate) refresh: bool, + pub(crate) refresh_package: Vec, + + // Shared settings. + pub(crate) shared: PipSharedSettings, +} + +impl PipSyncSettings { + /// Resolve the [`PipSyncSettings`] from the CLI and workspace configuration. + pub(crate) fn resolve(args: PipSyncArgs, workspace: Option) -> Self { + let PipSyncArgs { + src_file, + reinstall, + reinstall_package, + offline, + refresh, + refresh_package, + link_mode, + index_url, + extra_index_url, + find_links, + no_index, + index_strategy, + require_hashes, + keyring_provider, + python, + system, + break_system_packages, + legacy_setup_py, + no_build_isolation, + no_build, + no_binary, + only_binary, + compile, + no_compile: _, + config_setting, + strict, + compat_args: _, + } = args; + + Self { + // CLI-only settings. + src_file, + reinstall, + reinstall_package, + refresh, + refresh_package, + + // Shared settings. + shared: PipSharedSettings::combine( + PipOptions { + python, + system: Some(system), + break_system_packages: Some(break_system_packages), + offline: Some(offline), + index_url: index_url.and_then(Maybe::into_option), + extra_index_url: extra_index_url.map(|extra_index_urls| { + extra_index_urls + .into_iter() + .filter_map(Maybe::into_option) + .collect() + }), + no_index: Some(no_index), + find_links, + index_strategy, + keyring_provider, + no_build: Some(no_build), + no_binary, + only_binary, + no_build_isolation: Some(no_build_isolation), + strict: Some(strict), + legacy_setup_py: Some(legacy_setup_py), + config_settings: config_setting.map(|config_settings| { + config_settings.into_iter().collect::() + }), + link_mode, + compile_bytecode: Some(compile), + require_hashes: Some(require_hashes), + ..PipOptions::default() + }, + workspace, + ), + } + } +} + +/// The resolved settings to use for a `pip install` invocation. +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone)] +pub(crate) struct PipInstallSettings { + // CLI-only settings. + pub(crate) package: Vec, + pub(crate) requirement: Vec, + pub(crate) editable: Vec, + pub(crate) constraint: Vec, + pub(crate) r#override: Vec, + pub(crate) upgrade: bool, + pub(crate) upgrade_package: Vec, + pub(crate) reinstall: bool, + pub(crate) reinstall_package: Vec, + pub(crate) refresh: bool, + pub(crate) refresh_package: Vec, + pub(crate) dry_run: bool, + // Shared settings. + pub(crate) shared: PipSharedSettings, +} + +impl PipInstallSettings { + /// Resolve the [`PipInstallSettings`] from the CLI and workspace configuration. + pub(crate) fn resolve(args: PipInstallArgs, workspace: Option) -> Self { + let PipInstallArgs { + package, + requirement, + editable, + constraint, + r#override, + extra, + all_extras, + upgrade, + upgrade_package, + reinstall, + reinstall_package, + offline, + refresh, + refresh_package, + no_deps, + link_mode, + resolution, + prerelease, + pre, + index_url, + extra_index_url, + find_links, + no_index, + index_strategy, + require_hashes, + keyring_provider, + python, + system, + break_system_packages, + legacy_setup_py, + no_build_isolation, + no_build, + no_binary, + only_binary, + compile, + no_compile: _, + config_setting, + strict, + exclude_newer, + dry_run, + } = args; + + Self { + // CLI-only settings. + package, + requirement, + editable, + constraint, + r#override, + upgrade, + upgrade_package: upgrade_package.unwrap_or_default(), + reinstall, + reinstall_package: reinstall_package.unwrap_or_default(), + refresh, + refresh_package: refresh_package.unwrap_or_default(), + dry_run, + + // Shared settings. + shared: PipSharedSettings::combine( + PipOptions { + python, + system: Some(system), + break_system_packages: Some(break_system_packages), + offline: Some(offline), + index_url: index_url.and_then(Maybe::into_option), + extra_index_url: extra_index_url.map(|extra_index_urls| { + extra_index_urls + .into_iter() + .filter_map(Maybe::into_option) + .collect() + }), + no_index: Some(no_index), + find_links, + index_strategy, + keyring_provider, + no_build: Some(no_build), + no_binary, + only_binary, + no_build_isolation: Some(no_build_isolation), + strict: Some(strict), + extra, + all_extras: Some(all_extras), + no_deps: Some(no_deps), + resolution, + prerelease: if pre { + Some(PreReleaseMode::Allow) + } else { + prerelease + }, + legacy_setup_py: Some(legacy_setup_py), + config_settings: config_setting.map(|config_settings| { + config_settings.into_iter().collect::() + }), + exclude_newer, + link_mode, + compile_bytecode: Some(compile), + require_hashes: Some(require_hashes), + ..PipOptions::default() + }, + workspace, + ), + } + } +} + +/// The resolved settings to use for a `pip uninstall` invocation. +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone)] +pub(crate) struct PipUninstallSettings { + // CLI-only settings. + pub(crate) package: Vec, + pub(crate) requirement: Vec, + // Shared settings. + pub(crate) shared: PipSharedSettings, +} + +impl PipUninstallSettings { + /// Resolve the [`PipUninstallSettings`] from the CLI and workspace configuration. + pub(crate) fn resolve(args: PipUninstallArgs, workspace: Option) -> Self { + let PipUninstallArgs { + package, + requirement, + python, + keyring_provider, + system, + break_system_packages, + offline, + } = args; + + Self { + // CLI-only settings. + package, + requirement, + + // Shared settings. + shared: PipSharedSettings::combine( + PipOptions { + python, + system: Some(system), + break_system_packages: Some(break_system_packages), + offline: Some(offline), + keyring_provider, + ..PipOptions::default() + }, + workspace, + ), + } + } +} + +/// The resolved settings to use for a `pip freeze` invocation. +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone)] +pub(crate) struct PipFreezeSettings { + // CLI-only settings. + pub(crate) exclude_editable: bool, + // Shared settings. + pub(crate) shared: PipSharedSettings, +} + +impl PipFreezeSettings { + /// Resolve the [`PipFreezeSettings`] from the CLI and workspace configuration. + pub(crate) fn resolve(args: PipFreezeArgs, workspace: Option) -> Self { + let PipFreezeArgs { + exclude_editable, + strict, + python, + system, + } = args; + + Self { + // CLI-only settings. + exclude_editable, + + // Shared settings. + shared: PipSharedSettings::combine( + PipOptions { + python, + system: Some(system), + strict: Some(strict), + ..PipOptions::default() + }, + workspace, + ), + } + } +} + +/// The resolved settings to use for a `pip list` invocation. +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone)] +pub(crate) struct PipListSettings { + // CLI-only settings. + pub(crate) editable: bool, + pub(crate) exclude_editable: bool, + pub(crate) exclude: Vec, + pub(crate) format: ListFormat, + + // CLI-only settings. + pub(crate) shared: PipSharedSettings, +} + +impl PipListSettings { + /// Resolve the [`PipListSettings`] from the CLI and workspace configuration. + pub(crate) fn resolve(args: PipListArgs, workspace: Option) -> Self { + let PipListArgs { + editable, + exclude_editable, + exclude, + format, + strict, + python, + system, + compat_args: _, + } = args; + + Self { + // CLI-only settings. + editable, + exclude_editable, + exclude, + format, + + // Shared settings. + shared: PipSharedSettings::combine( + PipOptions { + python, + system: Some(system), + strict: Some(strict), + ..PipOptions::default() + }, + workspace, + ), + } + } +} + +/// The resolved settings to use for a `pip show` invocation. +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone)] +pub(crate) struct PipShowSettings { + // CLI-only settings. + pub(crate) package: Vec, + + // CLI-only settings. + pub(crate) shared: PipSharedSettings, +} + +impl PipShowSettings { + /// Resolve the [`PipShowSettings`] from the CLI and workspace configuration. + pub(crate) fn resolve(args: PipShowArgs, workspace: Option) -> Self { + let PipShowArgs { + package, + strict, + python, + system, + } = args; + + Self { + // CLI-only settings. + package, + + // Shared settings. + shared: PipSharedSettings::combine( + PipOptions { + python, + system: Some(system), + strict: Some(strict), + ..PipOptions::default() + }, + workspace, + ), + } + } +} + +/// The resolved settings to use for a `pip check` invocation. +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone)] +pub(crate) struct PipCheckSettings { + // CLI-only settings. + + // Shared settings. + pub(crate) shared: PipSharedSettings, +} + +impl PipCheckSettings { + /// Resolve the [`PipCheckSettings`] from the CLI and workspace configuration. + pub(crate) fn resolve(args: PipCheckArgs, workspace: Option) -> Self { + let PipCheckArgs { python, system } = args; + + Self { + // Shared settings. + shared: PipSharedSettings::combine( + PipOptions { + python, + system: Some(system), + ..PipOptions::default() + }, + workspace, + ), + } + } +} + +/// The resolved settings to use for a `pip check` invocation. +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone)] +pub(crate) struct VenvSettings { + // CLI-only settings. + pub(crate) seed: bool, + pub(crate) name: PathBuf, + pub(crate) prompt: Option, + pub(crate) system_site_packages: bool, + + // CLI-only settings. + pub(crate) shared: PipSharedSettings, +} + +impl VenvSettings { + /// Resolve the [`VenvSettings`] from the CLI and workspace configuration. + pub(crate) fn resolve(args: VenvArgs, workspace: Option) -> Self { + let VenvArgs { + python, + system, + seed, + name, + prompt, + system_site_packages, + link_mode, + index_url, + extra_index_url, + no_index, + index_strategy, + keyring_provider, + offline, + exclude_newer, + compat_args: _, + } = args; + + Self { + // CLI-only settings. + seed, + name, + prompt, + system_site_packages, + + // Shared settings. + shared: PipSharedSettings::combine( + PipOptions { + python, + system: Some(system), + offline: Some(offline), + index_url: index_url.and_then(Maybe::into_option), + extra_index_url: extra_index_url.map(|extra_index_urls| { + extra_index_urls + .into_iter() + .filter_map(Maybe::into_option) + .collect() + }), + no_index: Some(no_index), + index_strategy, + keyring_provider, + exclude_newer, + link_mode, + ..PipOptions::default() + }, + workspace, + ), + } + } +} + +/// The resolved settings to use for an invocation of the `pip` CLI. +/// +/// Represents the shared settings that are used across all `pip` commands. +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone)] +pub(crate) struct PipSharedSettings { + pub(crate) python: Option, + pub(crate) system: bool, + pub(crate) break_system_packages: bool, + pub(crate) offline: bool, + pub(crate) index_url: Option, + pub(crate) extra_index_url: Vec, + pub(crate) no_index: bool, + pub(crate) find_links: Vec, + pub(crate) index_strategy: IndexStrategy, + pub(crate) keyring_provider: KeyringProviderType, + pub(crate) no_build: bool, + pub(crate) no_binary: Vec, + pub(crate) only_binary: Vec, + pub(crate) no_build_isolation: bool, + pub(crate) strict: bool, + pub(crate) extra: Vec, + pub(crate) all_extras: bool, + pub(crate) no_deps: bool, + pub(crate) resolution: ResolutionMode, + pub(crate) prerelease: PreReleaseMode, + pub(crate) output_file: Option, + pub(crate) no_strip_extras: bool, + pub(crate) no_annotate: bool, + pub(crate) no_header: bool, + pub(crate) custom_compile_command: Option, + pub(crate) generate_hashes: bool, + pub(crate) legacy_setup_py: bool, + pub(crate) config_setting: ConfigSettings, + pub(crate) python_version: Option, + pub(crate) exclude_newer: Option, + pub(crate) no_emit_package: Vec, + pub(crate) emit_index_url: bool, + pub(crate) emit_find_links: bool, + pub(crate) emit_marker_expression: bool, + pub(crate) emit_index_annotation: bool, + pub(crate) annotation_style: AnnotationStyle, + pub(crate) link_mode: LinkMode, + pub(crate) compile_bytecode: bool, + pub(crate) require_hashes: bool, +} + +impl PipSharedSettings { + /// Resolve the [`PipSharedSettings`] from the CLI and workspace configuration. + pub(crate) fn combine(args: PipOptions, workspace: Option) -> Self { + let PipOptions { + python, + system, + break_system_packages, + offline, + index_url, + extra_index_url, + no_index, + find_links, + index_strategy, + keyring_provider, + no_build, + no_binary, + only_binary, + no_build_isolation, + strict, + extra, + all_extras, + no_deps, + resolution, + prerelease, + output_file, + no_strip_extras, + no_annotate, + no_header, + custom_compile_command, + generate_hashes, + legacy_setup_py, + config_settings, + python_version, + exclude_newer, + no_emit_package, + emit_index_url, + emit_find_links, + emit_marker_expression, + emit_index_annotation, + annotation_style, + link_mode, + compile_bytecode, + require_hashes, + } = workspace + .and_then(|workspace| workspace.options.pip) + .unwrap_or_default(); + + Self { + extra: args.extra.or(extra).unwrap_or_default(), + all_extras: args.all_extras.unwrap_or(false) || all_extras.unwrap_or(false), + no_deps: args.no_deps.unwrap_or(false) || no_deps.unwrap_or(false), + resolution: args.resolution.or(resolution).unwrap_or_default(), + prerelease: args.prerelease.or(prerelease).unwrap_or_default(), + output_file: args.output_file.or(output_file), + no_strip_extras: args.no_strip_extras.unwrap_or(false) + || no_strip_extras.unwrap_or(false), + no_annotate: args.no_annotate.unwrap_or(false) || no_annotate.unwrap_or(false), + no_header: args.no_header.unwrap_or(false) || no_header.unwrap_or(false), + custom_compile_command: args.custom_compile_command.or(custom_compile_command), + annotation_style: args + .annotation_style + .or(annotation_style) + .unwrap_or_default(), + offline: args.offline.unwrap_or(false) || offline.unwrap_or(false), + index_url: args.index_url.or(index_url), + extra_index_url: args.extra_index_url.or(extra_index_url).unwrap_or_default(), + no_index: args.no_index.unwrap_or(false) || no_index.unwrap_or(false), + index_strategy: args.index_strategy.or(index_strategy).unwrap_or_default(), + keyring_provider: args + .keyring_provider + .or(keyring_provider) + .unwrap_or_default(), + find_links: args.find_links.or(find_links).unwrap_or_default(), + generate_hashes: args.generate_hashes.unwrap_or(false) + || generate_hashes.unwrap_or(false), + legacy_setup_py: args.legacy_setup_py.unwrap_or(false) + || legacy_setup_py.unwrap_or(false), + no_build_isolation: args.no_build_isolation.unwrap_or(false) + || no_build_isolation.unwrap_or(false), + no_build: args.no_build.unwrap_or(false) || no_build.unwrap_or(false), + only_binary: args.only_binary.or(only_binary).unwrap_or_default(), + config_setting: args.config_settings.or(config_settings).unwrap_or_default(), + python_version: args.python_version.or(python_version), + exclude_newer: args.exclude_newer.or(exclude_newer), + no_emit_package: args.no_emit_package.or(no_emit_package).unwrap_or_default(), + emit_index_url: args.emit_index_url.unwrap_or(false) || emit_index_url.unwrap_or(false), + emit_find_links: args.emit_find_links.unwrap_or(false) + || emit_find_links.unwrap_or(false), + emit_marker_expression: args.emit_marker_expression.unwrap_or(false) + || emit_marker_expression.unwrap_or(false), + emit_index_annotation: args.emit_index_annotation.unwrap_or(false) + || emit_index_annotation.unwrap_or(false), + link_mode: args.link_mode.or(link_mode).unwrap_or_default(), + require_hashes: args.require_hashes.unwrap_or(false) || require_hashes.unwrap_or(false), + python: args.python.or(python), + system: args.system.unwrap_or(false) || system.unwrap_or(false), + break_system_packages: args.break_system_packages.unwrap_or(false) + || break_system_packages.unwrap_or(false), + no_binary: args.no_binary.or(no_binary).unwrap_or_default(), + compile_bytecode: args.compile_bytecode.unwrap_or(false) + || compile_bytecode.unwrap_or(false), + strict: args.strict.unwrap_or(false) || strict.unwrap_or(false), + } + } +}