Skip to content

Commit

Permalink
Merge user and workspace settings
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed May 8, 2024
1 parent 7c66321 commit 06cfd5a
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 8 deletions.
36 changes: 36 additions & 0 deletions crates/uv-configuration/src/config_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,42 @@ impl ConfigSettings {
pub fn escape_for_python(&self) -> String {
serde_json::to_string(self).expect("Failed to serialize config settings")
}

/// Merge two sets of config settings, with the values in `self` taking precedence.
#[must_use]
pub fn merge(self, other: ConfigSettings) -> ConfigSettings {
let mut config = self.0;
for (key, value) in other.0 {
match config.entry(key) {
Entry::Vacant(vacant) => {
vacant.insert(value);
}
Entry::Occupied(mut occupied) => match occupied.get_mut() {
ConfigSettingValue::String(existing) => {
let existing = existing.clone();
match value {
ConfigSettingValue::String(value) => {
occupied.insert(ConfigSettingValue::List(vec![existing, value]));
}
ConfigSettingValue::List(mut values) => {
values.insert(0, existing);
occupied.insert(ConfigSettingValue::List(values));
}
}
}
ConfigSettingValue::List(existing) => match value {
ConfigSettingValue::String(value) => {
existing.push(value);
}
ConfigSettingValue::List(values) => {
existing.extend(values);
}
},
},
}
}
Self(config)
}
}

impl serde::Serialize for ConfigSettings {
Expand Down
121 changes: 121 additions & 0 deletions crates/uv-workspace/src/combine.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use uv_configuration::ConfigSettings;

use crate::{Options, PipOptions, Workspace};

pub trait Combine {
/// Combine two values, preferring the values in `self`.
///
/// The logic should follow that of Cargo's `config.toml`:
///
/// > If a key is specified in multiple config files, the values will get merged together.
/// > Numbers, strings, and booleans will use the value in the deeper config directory taking
/// > precedence over ancestor directories, where the home directory is the lowest priority.
/// > Arrays will be joined together with higher precedence items being placed later in the
/// > merged array.
#[must_use]
fn combine(self, other: Self) -> Self;
}

impl Combine for Option<Workspace> {
/// Combine the options used in two [`Workspace`]s. Retains the root of `self`.
fn combine(self, other: Option<Workspace>) -> Option<Workspace> {
match (self, other) {
(Some(mut a), Some(b)) => {
a.options = a.options.combine(b.options);
Some(a)
}
(a, b) => a.or(b),
}
}
}

impl Combine for Options {
fn combine(self, other: Options) -> Options {
Options {
native_tls: self.native_tls.or(other.native_tls),
no_cache: self.no_cache.or(other.no_cache),
preview: self.preview.or(other.preview),
cache_dir: self.cache_dir.or(other.cache_dir),
pip: match (self.pip, other.pip) {
(Some(a), Some(b)) => Some(a.combine(b)),
(a, b) => a.or(b),
},
}
}
}

impl Combine for PipOptions {
fn combine(self, other: PipOptions) -> PipOptions {
PipOptions {
// Collection types, which must be merged element-wise.
extra_index_url: self.extra_index_url.combine(other.extra_index_url),
find_links: self.find_links.combine(other.find_links),
no_binary: self.no_binary.combine(other.no_binary),
only_binary: self.only_binary.combine(other.only_binary),
extra: self.extra.combine(other.extra),
config_settings: self.config_settings.combine(other.config_settings),
no_emit_package: self.no_emit_package.combine(other.no_emit_package),

// Non-collections, where the last value wins.
python: self.python.or(other.python),
system: self.system.or(other.system),
break_system_packages: self.break_system_packages.or(other.break_system_packages),
target: self.target.or(other.target),
offline: self.offline.or(other.offline),
index_url: self.index_url.or(other.index_url),
no_index: self.no_index.or(other.no_index),
index_strategy: self.index_strategy.or(other.index_strategy),
keyring_provider: self.keyring_provider.or(other.keyring_provider),
no_build: self.no_build.or(other.no_build),
no_build_isolation: self.no_build_isolation.or(other.no_build_isolation),
strict: self.strict.or(other.strict),
all_extras: self.all_extras.or(other.all_extras),
no_deps: self.no_deps.or(other.no_deps),
resolution: self.resolution.or(other.resolution),
prerelease: self.prerelease.or(other.prerelease),
output_file: self.output_file.or(other.output_file),
no_strip_extras: self.no_strip_extras.or(other.no_strip_extras),
no_annotate: self.no_annotate.or(other.no_annotate),
no_header: self.no_header.or(other.no_header),
custom_compile_command: self.custom_compile_command.or(other.custom_compile_command),
generate_hashes: self.generate_hashes.or(other.generate_hashes),
legacy_setup_py: self.legacy_setup_py.or(other.legacy_setup_py),
python_version: self.python_version.or(other.python_version),
python_platform: self.python_platform.or(other.python_platform),
exclude_newer: self.exclude_newer.or(other.exclude_newer),
emit_index_url: self.emit_index_url.or(other.emit_index_url),
emit_find_links: self.emit_find_links.or(other.emit_find_links),
emit_marker_expression: self.emit_marker_expression.or(other.emit_marker_expression),
emit_index_annotation: self.emit_index_annotation.or(other.emit_index_annotation),
annotation_style: self.annotation_style.or(other.annotation_style),
link_mode: self.link_mode.or(other.link_mode),
compile_bytecode: self.compile_bytecode.or(other.compile_bytecode),
require_hashes: self.require_hashes.or(other.require_hashes),
}
}
}

impl<T> Combine for Option<Vec<T>> {
/// Combine two vectors by extending the vector in `self` with the vector in `other`, if they're
/// both `Some`.
fn combine(self, other: Option<Vec<T>>) -> Option<Vec<T>> {
match (self, other) {
(Some(mut a), Some(b)) => {
a.extend(b);
Some(a)
}
(a, b) => a.or(b),
}
}
}

impl Combine for Option<ConfigSettings> {
/// Combine two maps by merging the map in `self` with the map in `other`, if they're both
/// `Some`.
fn combine(self, other: Option<ConfigSettings>) -> Option<ConfigSettings> {
match (self, other) {
(Some(a), Some(b)) => Some(a.merge(b)),
(a, b) => a.or(b),
}
}
}
2 changes: 2 additions & 0 deletions crates/uv-workspace/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
pub use crate::combine::*;
pub use crate::settings::*;
pub use crate::workspace::*;

mod combine;
mod settings;
mod workspace;
13 changes: 9 additions & 4 deletions crates/uv-workspace/src/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ impl Workspace {
};
let root = dir.join("uv");
let file = root.join("uv.toml");
Ok(Some(Self {
options: read_file(&file).unwrap_or_default(),
root,
}))

debug!("Loading user configuration from: `{}`", file.display());
match read_file(&file) {
Ok(options) => Ok(Some(Self { options, root })),
Err(WorkspaceError::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(err) => Err(err),
}
}

/// Find the [`Workspace`] for the given path.
Expand Down Expand Up @@ -137,6 +140,8 @@ fn find_in_directory(dir: &Path) -> Result<Option<Options>, WorkspaceError> {
}

/// Load [`Options`] from a `pyproject.toml` or `uv.toml` file.
///
/// If the file does not exist, `Ok(None)` is returned.
fn read_file(path: &Path) -> Result<Options, WorkspaceError> {
let content = fs_err::read_to_string(path)?;
if path.ends_with("pyproject.toml") {
Expand Down
8 changes: 4 additions & 4 deletions crates/uv/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use owo_colors::OwoColorize;
use tracing::instrument;

use uv_cache::Cache;

use uv_requirements::RequirementsSource;
use uv_workspace::Combine;

use crate::cli::{CacheCommand, CacheNamespace, Cli, Commands, PipCommand, PipNamespace};
#[cfg(feature = "self-update")]
Expand Down Expand Up @@ -114,10 +114,10 @@ async fn run() -> Result<ExitStatus> {
Some(uv_workspace::Workspace::from_file(config_file)?)
} else if cli.isolated {
None
} else if let Some(workspace) = uv_workspace::Workspace::find(env::current_dir()?)? {
Some(workspace)
} else {
uv_workspace::Workspace::user()?
let project = uv_workspace::Workspace::find(env::current_dir()?)?;
let user = uv_workspace::Workspace::user()?;
project.combine(user)
};

// Resolve the global settings.
Expand Down

0 comments on commit 06cfd5a

Please sign in to comment.