use std::{
collections::{BTreeMap, btree_map::Entry},
str::FromStr,
};
use uv_cache_key::CacheKeyHasher;
use uv_normalize::PackageName;
#[derive(Debug, Clone)]
pub struct ConfigSettingEntry {
key: String,
value: String,
}
impl FromStr for ConfigSettingEntry {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Some((key, value)) = s.split_once('=') else {
return Err(format!(
"Invalid config setting: {s} (expected `KEY=VALUE`)"
));
};
Ok(Self {
key: key.trim().to_string(),
value: value.trim().to_string(),
})
}
}
#[derive(Debug, Clone)]
pub struct ConfigSettingPackageEntry {
package: PackageName,
setting: ConfigSettingEntry,
}
impl FromStr for ConfigSettingPackageEntry {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Some((package_str, config_str)) = s.split_once(':') else {
return Err(format!(
"Invalid config setting: {s} (expected `PACKAGE:KEY=VALUE`)"
));
};
let package = PackageName::from_str(package_str.trim())
.map_err(|e| format!("Invalid package name: {e}"))?;
let setting = ConfigSettingEntry::from_str(config_str)?;
Ok(Self { package, setting })
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema), schemars(untagged))]
enum ConfigSettingValue {
String(String),
List(Vec<String>),
}
impl serde::Serialize for ConfigSettingValue {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Self::String(value) => serializer.serialize_str(value),
Self::List(values) => serializer.collect_seq(values.iter()),
}
}
}
impl<'de> serde::Deserialize<'de> for ConfigSettingValue {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = ConfigSettingValue;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string or list of strings")
}
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
Ok(ConfigSettingValue::String(value.to_string()))
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(
self,
mut seq: A,
) -> Result<Self::Value, A::Error> {
let mut values = Vec::new();
while let Some(value) = seq.next_element()? {
values.push(value);
}
Ok(ConfigSettingValue::List(values))
}
}
deserializer.deserialize_any(Visitor)
}
}
#[derive(Debug, Default, Hash, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ConfigSettings(BTreeMap<String, ConfigSettingValue>);
impl FromIterator<ConfigSettingEntry> for ConfigSettings {
fn from_iter<T: IntoIterator<Item = ConfigSettingEntry>>(iter: T) -> Self {
let mut config = BTreeMap::default();
for entry in iter {
match config.entry(entry.key) {
Entry::Vacant(vacant) => {
vacant.insert(ConfigSettingValue::String(entry.value));
}
Entry::Occupied(mut occupied) => match occupied.get_mut() {
ConfigSettingValue::String(existing) => {
let existing = existing.clone();
occupied.insert(ConfigSettingValue::List(vec![existing, entry.value]));
}
ConfigSettingValue::List(existing) => {
existing.push(entry.value);
}
},
}
}
Self(config)
}
}
impl ConfigSettings {
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn escape_for_python(&self) -> String {
serde_json::to_string(self).expect("Failed to serialize config settings")
}
#[must_use]
pub fn merge(self, other: Self) -> Self {
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 uv_cache_key::CacheKey for ConfigSettings {
fn cache_key(&self, state: &mut CacheKeyHasher) {
for (key, value) in &self.0 {
key.cache_key(state);
match value {
ConfigSettingValue::String(value) => value.cache_key(state),
ConfigSettingValue::List(values) => values.cache_key(state),
}
}
}
}
impl serde::Serialize for ConfigSettings {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(Some(self.0.len()))?;
for (key, value) in &self.0 {
map.serialize_entry(key, value)?;
}
map.end()
}
}
impl<'de> serde::Deserialize<'de> for ConfigSettings {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = ConfigSettings;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a map from string to string or list of strings")
}
fn visit_map<A: serde::de::MapAccess<'de>>(
self,
mut map: A,
) -> Result<Self::Value, A::Error> {
let mut config = BTreeMap::default();
while let Some((key, value)) = map.next_entry()? {
config.insert(key, value);
}
Ok(ConfigSettings(config))
}
}
deserializer.deserialize_map(Visitor)
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct PackageConfigSettings(BTreeMap<PackageName, ConfigSettings>);
impl FromIterator<ConfigSettingPackageEntry> for PackageConfigSettings {
fn from_iter<T: IntoIterator<Item = ConfigSettingPackageEntry>>(iter: T) -> Self {
let mut package_configs: BTreeMap<PackageName, Vec<ConfigSettingEntry>> = BTreeMap::new();
for entry in iter {
package_configs
.entry(entry.package)
.or_default()
.push(entry.setting);
}
let configs = package_configs
.into_iter()
.map(|(package, entries)| (package, entries.into_iter().collect()))
.collect();
Self(configs)
}
}
impl PackageConfigSettings {
pub fn get(&self, package: &PackageName) -> Option<&ConfigSettings> {
self.0.get(package)
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[must_use]
pub fn merge(mut self, other: Self) -> Self {
for (package, settings) in other.0 {
match self.0.entry(package) {
Entry::Vacant(vacant) => {
vacant.insert(settings);
}
Entry::Occupied(mut occupied) => {
let merged = occupied.get().clone().merge(settings);
occupied.insert(merged);
}
}
}
self
}
}
impl uv_cache_key::CacheKey for PackageConfigSettings {
fn cache_key(&self, state: &mut CacheKeyHasher) {
for (package, settings) in &self.0 {
package.to_string().cache_key(state);
settings.cache_key(state);
}
}
}
impl serde::Serialize for PackageConfigSettings {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(Some(self.0.len()))?;
for (key, value) in &self.0 {
map.serialize_entry(&key.to_string(), value)?;
}
map.end()
}
}
impl<'de> serde::Deserialize<'de> for PackageConfigSettings {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = PackageConfigSettings;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a map from package name to config settings")
}
fn visit_map<A: serde::de::MapAccess<'de>>(
self,
mut map: A,
) -> Result<Self::Value, A::Error> {
let mut config = BTreeMap::default();
while let Some((key, value)) = map.next_entry::<String, ConfigSettings>()? {
let package = PackageName::from_str(&key).map_err(|e| {
serde::de::Error::custom(format!("Invalid package name: {e}"))
})?;
config.insert(package, value);
}
Ok(PackageConfigSettings(config))
}
}
deserializer.deserialize_map(Visitor)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn collect_config_settings() {
let settings: ConfigSettings = vec![
ConfigSettingEntry {
key: "key".to_string(),
value: "value".to_string(),
},
ConfigSettingEntry {
key: "key".to_string(),
value: "value2".to_string(),
},
ConfigSettingEntry {
key: "list".to_string(),
value: "value3".to_string(),
},
ConfigSettingEntry {
key: "list".to_string(),
value: "value4".to_string(),
},
]
.into_iter()
.collect();
assert_eq!(
settings.0.get("key"),
Some(&ConfigSettingValue::List(vec![
"value".to_string(),
"value2".to_string()
]))
);
assert_eq!(
settings.0.get("list"),
Some(&ConfigSettingValue::List(vec![
"value3".to_string(),
"value4".to_string()
]))
);
}
#[test]
fn escape_for_python() {
let mut settings = ConfigSettings::default();
settings.0.insert(
"key".to_string(),
ConfigSettingValue::String("value".to_string()),
);
settings.0.insert(
"list".to_string(),
ConfigSettingValue::List(vec!["value1".to_string(), "value2".to_string()]),
);
assert_eq!(
settings.escape_for_python(),
r#"{"key":"value","list":["value1","value2"]}"#
);
let mut settings = ConfigSettings::default();
settings.0.insert(
"key".to_string(),
ConfigSettingValue::String("Hello, \"world!\"".to_string()),
);
settings.0.insert(
"list".to_string(),
ConfigSettingValue::List(vec!["'value1'".to_string()]),
);
assert_eq!(
settings.escape_for_python(),
r#"{"key":"Hello, \"world!\"","list":["'value1'"]}"#
);
let mut settings = ConfigSettings::default();
settings.0.insert(
"key".to_string(),
ConfigSettingValue::String("val\\1 {}value".to_string()),
);
assert_eq!(settings.escape_for_python(), r#"{"key":"val\\1 {}value"}"#);
}
#[test]
fn parse_config_setting_package_entry() {
let entry = ConfigSettingPackageEntry::from_str("numpy:editable_mode=compat").unwrap();
assert_eq!(entry.package.as_ref(), "numpy");
assert_eq!(entry.setting.key, "editable_mode");
assert_eq!(entry.setting.value, "compat");
let entry = ConfigSettingPackageEntry::from_str("my-package:some_key=value").unwrap();
assert_eq!(entry.package.as_ref(), "my-package");
assert_eq!(entry.setting.key, "some_key");
assert_eq!(entry.setting.value, "value");
let entry = ConfigSettingPackageEntry::from_str(" numpy : key = value ").unwrap();
assert_eq!(entry.package.as_ref(), "numpy");
assert_eq!(entry.setting.key, "key");
assert_eq!(entry.setting.value, "value");
}
#[test]
fn collect_config_settings_package() {
let settings: PackageConfigSettings = vec![
ConfigSettingPackageEntry::from_str("numpy:editable_mode=compat").unwrap(),
ConfigSettingPackageEntry::from_str("numpy:another_key=value").unwrap(),
ConfigSettingPackageEntry::from_str("scipy:build_option=fast").unwrap(),
]
.into_iter()
.collect();
let numpy_settings = settings
.get(&PackageName::from_str("numpy").unwrap())
.unwrap();
assert_eq!(
numpy_settings.0.get("editable_mode"),
Some(&ConfigSettingValue::String("compat".to_string()))
);
assert_eq!(
numpy_settings.0.get("another_key"),
Some(&ConfigSettingValue::String("value".to_string()))
);
let scipy_settings = settings
.get(&PackageName::from_str("scipy").unwrap())
.unwrap();
assert_eq!(
scipy_settings.0.get("build_option"),
Some(&ConfigSettingValue::String("fast".to_string()))
);
}
}