From f3bc067f090b13420ab88acb2bb185cd792b075c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 15 Apr 2024 14:05:59 -0400 Subject: [PATCH] Add settings struct --- crates/uv-cache/src/cli.rs | 38 +++-- crates/uv-workspace/src/settings.rs | 14 +- crates/uv-workspace/src/workspace.rs | 4 +- crates/uv/src/cli.rs | 55 +++--- crates/uv/src/commands/pip_compile.rs | 2 +- crates/uv/src/main.rs | 41 +++-- crates/uv/src/settings.rs | 235 ++++++++++++++++++++++++++ 7 files changed, 328 insertions(+), 61 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 18d04ab5891f0..19780139eba73 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 5e8ff6a314833..2b1bf1f006169 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, } @@ -44,7 +42,7 @@ pub struct PipOptions { pub system: 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, @@ -62,8 +60,12 @@ pub struct PipOptions { #[derive(Debug, Clone, Default, Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] pub struct ResolverOptions { + 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, @@ -75,6 +77,8 @@ pub struct ResolverOptions { 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, } diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index 17d0b8f276c50..979a94ecb136d 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -9,8 +9,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 4f8b27cabe036..7ee4d447901df 100644 --- a/crates/uv/src/cli.rs +++ b/crates/uv/src/cli.rs @@ -246,7 +246,7 @@ pub(crate) struct PipCompileArgs { /// Include optional dependencies in the given extra group name; may be provided more than once. #[clap(long, conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)] - pub(crate) extra: Vec, + pub(crate) extra: Option>, /// Include all optional dependencies. #[clap(long, conflicts_with = "extra")] @@ -257,11 +257,16 @@ pub(crate) struct PipCompileArgs { #[clap(long)] pub(crate) no_deps: bool, - #[clap(long, value_enum, default_value_t = ResolutionMode::default(), env = "UV_RESOLUTION")] - pub(crate) resolution: ResolutionMode, + #[clap(long, value_enum, default_value = "highest", env = "UV_RESOLUTION")] + pub(crate) resolution: Option, - #[clap(long, value_enum, default_value_t = PreReleaseMode::default(), env = "UV_PRERELEASE")] - pub(crate) prerelease: PreReleaseMode, + #[clap( + long, + value_enum, + default_value = "if-necessary-or-explicit", + env = "UV_PRERELEASE" + )] + pub(crate) prerelease: Option, #[clap(long, hide = true)] pub(crate) pre: bool, @@ -287,8 +292,8 @@ pub(crate) struct PipCompileArgs { pub(crate) no_header: bool, /// Choose the style of the annotation comments, which indicate the source of each package. - #[clap(long, default_value_t=AnnotationStyle::Split, value_enum)] - pub(crate) annotation_style: AnnotationStyle, + #[clap(long, default_value = "split", value_enum)] + pub(crate) annotation_style: Option, /// Change header comment to reflect custom command wrapping `uv pip compile`. #[clap(long, env = "UV_CUSTOM_COMPILE_COMMAND")] @@ -309,7 +314,7 @@ pub(crate) struct PipCompileArgs { /// Refresh cached data for a specific package. #[clap(long)] - pub(crate) refresh_package: Vec, + pub(crate) refresh_package: Option>, /// The method to use when installing packages from the global cache. /// @@ -317,8 +322,8 @@ pub(crate) struct PipCompileArgs { /// /// Defaults to `clone` (also known as Copy-on-Write) on macOS, and `hardlink` on Linux and /// Windows. - #[clap(long, value_enum, default_value_t = install_wheel_rs::linker::LinkMode::default())] - pub(crate) link_mode: install_wheel_rs::linker::LinkMode, + #[clap(long, value_enum)] + pub(crate) link_mode: Option, /// The URL of the Python package index (by default: ). /// @@ -341,7 +346,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. #[clap(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`. @@ -354,15 +359,25 @@ pub(crate) struct PipCompileArgs { /// 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. - #[clap(long, default_value_t, value_enum, env = "UV_INDEX_STRATEGY")] - pub(crate) index_strategy: IndexStrategy, + #[clap( + long, + default_value = "first-match", + value_enum, + env = "UV_INDEX_STRATEGY" + )] + pub(crate) index_strategy: Option, /// 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. - #[clap(long, default_value_t, value_enum, env = "UV_KEYRING_PROVIDER")] - pub(crate) keyring_provider: KeyringProviderType, + #[clap( + long, + default_value = "disabled", + value_enum, + env = "UV_KEYRING_PROVIDER" + )] + pub(crate) keyring_provider: Option, /// Locations to search for candidate distributions, beyond those found in the indexes. /// @@ -371,7 +386,7 @@ pub(crate) struct PipCompileArgs { /// /// If a URL, the page must contain a flat list of links to package files. #[clap(long, short)] - pub(crate) find_links: Vec, + pub(crate) find_links: Option>, /// Allow package upgrades, ignoring pinned versions in the existing output file. #[clap(long, short = 'U')] @@ -380,7 +395,7 @@ pub(crate) struct PipCompileArgs { /// Allow upgrades for a specific package, ignoring pinned versions in the existing output /// file. #[clap(long, short = 'P')] - pub(crate) upgrade_package: Vec, + pub(crate) upgrade_package: Option>, /// Include distribution hashes in the output file. #[clap(long)] @@ -416,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:`. #[clap(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. #[clap(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`). @@ -440,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. #[clap(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. #[clap(long)] diff --git a/crates/uv/src/commands/pip_compile.rs b/crates/uv/src/commands/pip_compile.rs index b86c80fc98bb0..8f99152a83caa 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 d62f9d28a6118..07b7c02d3a73c 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -22,6 +22,7 @@ use crate::cli::{CacheCommand, CacheNamespace, Cli, Commands, Maybe, PipCommand, use crate::cli::{SelfCommand, SelfNamespace}; use crate::commands::ExitStatus; use crate::compat::CompatArgs; +use crate::settings::{CacheSettings, GlobalSettings, PipCompileSettings}; #[cfg(target_os = "windows")] #[global_allocator] @@ -44,6 +45,7 @@ mod commands; mod compat; mod logging; mod printer; +mod settings; mod shell; mod version; @@ -105,9 +107,10 @@ async fn run() -> Result { }; // Load the workspace settings. - let _ = uv_workspace::Workspace::find(env::current_dir()?)?; + let workspace = uv_workspace::Workspace::find(env::current_dir()?)?; - let globals = cli.global_args; + // 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")] @@ -137,11 +140,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( @@ -154,7 +153,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 { @@ -162,6 +163,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 @@ -179,14 +183,12 @@ 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.index_url, + args.extra_index_url, args.find_links, args.no_index, ); + // TODO(charlie): Move into `PipCompileSettings::resolve`. let extras = if args.all_extras { ExtrasSpecification::All } else if args.extra.is_empty() { @@ -201,17 +203,12 @@ async fn run() -> Result { } else { DependencyMode::Transitive }; - let prerelease = if args.pre { - PreReleaseMode::Allow - } else { - args.prerelease - }; let setup_py = if args.legacy_setup_py { SetupPyStrategy::Setuptools } else { SetupPyStrategy::Pep517 }; - let config_settings = args.config_setting.into_iter().collect::(); + commands::pip_compile( &requirements, &constraints, @@ -219,7 +216,7 @@ async fn run() -> Result { extras, args.output_file.as_deref(), args.resolution, - prerelease, + args.prerelease, dependency_mode, upgrade, args.generate_hashes, @@ -236,7 +233,7 @@ async fn run() -> Result { args.index_strategy, args.keyring_provider, setup_py, - config_settings, + args.config_setting, if args.offline { Connectivity::Offline } else { @@ -247,9 +244,9 @@ async fn run() -> Result { args.python_version, args.exclude_newer, args.annotation_style, + args.link_mode, globals.native_tls, globals.quiet, - args.link_mode, cache, printer, ) diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs new file mode 100644 index 0000000000000..d3a1760df1982 --- /dev/null +++ b/crates/uv/src/settings.rs @@ -0,0 +1,235 @@ +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::{InstallerOptions, PipOptions, ResolverOptions, Workspace}; + +use crate::cli::{ColorChoice, GlobalArgs, Maybe, PipCompileArgs}; + +/// The resolved global settings to use for any invocation of the CLI. +#[allow(clippy::struct_excessive_bools)] +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)] +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. +/// +/// Represents the resolution of the settings from the command-line arguments and the on-disk +/// workspace configuration. +/// +/// Responsible for both (1) reconciling the command-line arguments and workspace configuration, +/// and (2) setting default values for any settings that are not provided. +#[allow(clippy::struct_excessive_bools)] +pub(crate) struct PipCompileSettings { + pub(crate) src_file: Vec, + pub(crate) constraint: Vec, + pub(crate) r#override: Vec, + 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) annotation_style: AnnotationStyle, + pub(crate) custom_compile_command: Option, + pub(crate) offline: bool, + pub(crate) refresh: bool, + pub(crate) refresh_package: Vec, + pub(crate) index_url: Option, + pub(crate) extra_index_url: Vec, + pub(crate) no_index: bool, + pub(crate) index_strategy: IndexStrategy, + pub(crate) keyring_provider: KeyringProviderType, + pub(crate) find_links: Vec, + pub(crate) upgrade: bool, + pub(crate) upgrade_package: Vec, + pub(crate) generate_hashes: bool, + pub(crate) legacy_setup_py: bool, + pub(crate) no_build_isolation: bool, + pub(crate) no_build: bool, + pub(crate) only_binary: Vec, + 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) link_mode: LinkMode, +} + +impl PipCompileSettings { + /// Resolve the [`PipCompileSettings`] from the CLI and workspace configuration. + pub(crate) fn resolve(args: PipCompileArgs, workspace: Option) -> Self { + let PipOptions { + system: _, + offline, + index_url, + extra_index_url, + no_index, + find_links, + index_strategy, + keyring_provider, + no_build, + no_binary: _, + only_binary, + no_build_isolation, + resolver, + installer, + } = workspace + .and_then(|workspace| workspace.options.pip) + .unwrap_or_default(); + + let ResolverOptions { + extra, + all_extras, + no_deps, + resolution, + prerelease, + output_file, + no_strip_extras, + no_annotate, + no_header, + generate_hashes, + legacy_setup_py, + config_setting, + python_version, + exclude_newer, + no_emit_package, + emit_index_url, + emit_find_links, + emit_marker_expression, + emit_index_annotation, + annotation_style, + } = resolver.unwrap_or_default(); + + let InstallerOptions { + link_mode, + compile_bytecode: _, + } = installer.unwrap_or_default(); + + Self { + // CLI-only settings. + src_file: args.src_file, + constraint: args.constraint, + r#override: args.r#override, + refresh: args.refresh, + refresh_package: args.refresh_package.unwrap_or_default(), + upgrade: args.upgrade, + upgrade_package: args.upgrade_package.unwrap_or_default(), + + // Resolver settings. + extra: args.extra.or(extra).unwrap_or_default(), + all_extras: args.all_extras || all_extras.unwrap_or(false), + no_deps: args.no_deps || no_deps.unwrap_or(false), + resolution: args.resolution.or(resolution).unwrap_or_default(), + prerelease: if args.pre { + PreReleaseMode::Allow + } else { + args.prerelease.or(prerelease).unwrap_or_default() + }, + output_file: args.output_file.or(output_file), + no_strip_extras: args.no_strip_extras || no_strip_extras.unwrap_or(false), + no_annotate: args.no_annotate || no_annotate.unwrap_or(false), + no_header: args.no_header || no_header.unwrap_or(false), + annotation_style: args + .annotation_style + .or(annotation_style) + .unwrap_or_default(), + custom_compile_command: args.custom_compile_command, + offline: args.offline || offline.unwrap_or(false), + index_url: args.index_url.and_then(Maybe::into_option).or(index_url), + extra_index_url: args + .extra_index_url + .map(|extra_index_urls| { + extra_index_urls + .into_iter() + .filter_map(Maybe::into_option) + .collect() + }) + .or(extra_index_url) + .unwrap_or_default(), + no_index: args.no_index || 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 || generate_hashes.unwrap_or(false), + legacy_setup_py: args.legacy_setup_py || legacy_setup_py.unwrap_or(false), + no_build_isolation: args.no_build_isolation || no_build_isolation.unwrap_or(false), + no_build: args.no_build || no_build.unwrap_or(false), + only_binary: args.only_binary.or(only_binary).unwrap_or_default(), + config_setting: args + .config_setting + .map(|config_settings| config_settings.into_iter().collect::()) + .or(config_setting) + .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 || emit_index_url.unwrap_or(false), + emit_find_links: args.emit_find_links || emit_find_links.unwrap_or(false), + emit_marker_expression: args.emit_marker_expression + || emit_marker_expression.unwrap_or(false), + emit_index_annotation: args.emit_index_annotation + || emit_index_annotation.unwrap_or(false), + + // Installer settings. + link_mode: args.link_mode.or(link_mode).unwrap_or_default(), + } + } +}