Skip to main content

uv_distribution_types/
config_settings.rs

1use std::{
2    collections::{BTreeMap, btree_map::Entry},
3    mem,
4    str::FromStr,
5};
6use uv_cache_key::CacheKeyHasher;
7use uv_normalize::PackageName;
8
9#[derive(Debug, Clone)]
10pub struct ConfigSettingEntry {
11    /// The key of the setting. For example, given `key=value`, this would be `key`.
12    key: String,
13    /// The value of the setting. For example, given `key=value`, this would be `value`.
14    value: String,
15}
16
17impl FromStr for ConfigSettingEntry {
18    type Err = String;
19
20    fn from_str(s: &str) -> Result<Self, Self::Err> {
21        let Some((key, value)) = s.split_once('=') else {
22            return Err(format!(
23                "Invalid config setting: {s} (expected `KEY=VALUE`)"
24            ));
25        };
26        Ok(Self {
27            key: key.trim().to_string(),
28            value: value.trim().to_string(),
29        })
30    }
31}
32
33#[derive(Debug, Clone)]
34pub struct ConfigSettingPackageEntry {
35    /// The package name to apply the setting to.
36    package: PackageName,
37    /// The config setting entry.
38    setting: ConfigSettingEntry,
39}
40
41impl FromStr for ConfigSettingPackageEntry {
42    type Err = String;
43
44    fn from_str(s: &str) -> Result<Self, Self::Err> {
45        let Some((package_str, config_str)) = s.split_once(':') else {
46            return Err(format!(
47                "Invalid config setting: {s} (expected `PACKAGE:KEY=VALUE`)"
48            ));
49        };
50
51        let package = PackageName::from_str(package_str.trim())
52            .map_err(|e| format!("Invalid package name: {e}"))?;
53        let setting = ConfigSettingEntry::from_str(config_str)?;
54
55        Ok(Self { package, setting })
56    }
57}
58
59#[derive(Debug, Clone, Hash, PartialEq, Eq)]
60#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema), schemars(untagged))]
61enum ConfigSettingValue {
62    /// The value consists of a single string.
63    String(String),
64    /// The value consists of a list of strings.
65    List(Vec<String>),
66}
67
68impl serde::Serialize for ConfigSettingValue {
69    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
70        match self {
71            Self::String(value) => serializer.serialize_str(value),
72            Self::List(values) => serializer.collect_seq(values.iter()),
73        }
74    }
75}
76
77impl<'de> serde::Deserialize<'de> for ConfigSettingValue {
78    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
79        struct Visitor;
80
81        impl<'de> serde::de::Visitor<'de> for Visitor {
82            type Value = ConfigSettingValue;
83
84            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
85                formatter.write_str("a string or list of strings")
86            }
87
88            fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
89                Ok(ConfigSettingValue::String(value.to_string()))
90            }
91
92            fn visit_string<E: serde::de::Error>(self, value: String) -> Result<Self::Value, E> {
93                Ok(ConfigSettingValue::String(value))
94            }
95
96            fn visit_seq<A: serde::de::SeqAccess<'de>>(
97                self,
98                mut seq: A,
99            ) -> Result<Self::Value, A::Error> {
100                let mut values = Vec::new();
101                while let Some(value) = seq.next_element()? {
102                    values.push(value);
103                }
104                Ok(ConfigSettingValue::List(values))
105            }
106        }
107
108        deserializer.deserialize_any(Visitor)
109    }
110}
111
112/// Settings to pass to a PEP 517 build backend, structured as a map from (string) key to string or
113/// list of strings.
114///
115/// See: <https://peps.python.org/pep-0517/#config-settings>
116#[derive(Debug, Default, Hash, Clone, PartialEq, Eq)]
117#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
118pub struct ConfigSettings(BTreeMap<String, ConfigSettingValue>);
119
120impl FromIterator<ConfigSettingEntry> for ConfigSettings {
121    fn from_iter<T: IntoIterator<Item = ConfigSettingEntry>>(iter: T) -> Self {
122        let mut config = BTreeMap::default();
123        for entry in iter {
124            match config.entry(entry.key) {
125                Entry::Vacant(vacant) => {
126                    vacant.insert(ConfigSettingValue::String(entry.value));
127                }
128                Entry::Occupied(mut occupied) => match occupied.get_mut() {
129                    ConfigSettingValue::String(existing) => {
130                        let existing = mem::take(existing);
131                        occupied.insert(ConfigSettingValue::List(vec![existing, entry.value]));
132                    }
133                    ConfigSettingValue::List(existing) => {
134                        existing.push(entry.value);
135                    }
136                },
137            }
138        }
139        Self(config)
140    }
141}
142
143impl ConfigSettings {
144    /// Returns `true` if the configuration contains no settings.
145    pub(crate) fn is_empty(&self) -> bool {
146        self.0.is_empty()
147    }
148
149    /// Convert the settings to a string that can be passed directly to a PEP 517 build backend.
150    pub fn escape_for_python(&self) -> String {
151        serde_json::to_string(self).expect("Failed to serialize config settings")
152    }
153
154    /// Merge two sets of config settings, with the values in `self` taking precedence.
155    #[must_use]
156    pub fn merge(self, other: Self) -> Self {
157        let mut config = self.0;
158        for (key, value) in other.0 {
159            match config.entry(key) {
160                Entry::Vacant(vacant) => {
161                    vacant.insert(value);
162                }
163                Entry::Occupied(mut occupied) => match occupied.get_mut() {
164                    ConfigSettingValue::String(existing) => {
165                        let existing = mem::take(existing);
166                        match value {
167                            ConfigSettingValue::String(value) => {
168                                occupied.insert(ConfigSettingValue::List(vec![existing, value]));
169                            }
170                            ConfigSettingValue::List(mut values) => {
171                                values.insert(0, existing);
172                                occupied.insert(ConfigSettingValue::List(values));
173                            }
174                        }
175                    }
176                    ConfigSettingValue::List(existing) => match value {
177                        ConfigSettingValue::String(value) => {
178                            existing.push(value);
179                        }
180                        ConfigSettingValue::List(values) => {
181                            existing.extend(values);
182                        }
183                    },
184                },
185            }
186        }
187        Self(config)
188    }
189}
190
191impl uv_cache_key::CacheKey for ConfigSettings {
192    fn cache_key(&self, state: &mut CacheKeyHasher) {
193        for (key, value) in &self.0 {
194            key.cache_key(state);
195            match value {
196                ConfigSettingValue::String(value) => value.cache_key(state),
197                ConfigSettingValue::List(values) => values.cache_key(state),
198            }
199        }
200    }
201}
202
203impl serde::Serialize for ConfigSettings {
204    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
205        use serde::ser::SerializeMap;
206
207        let mut map = serializer.serialize_map(Some(self.0.len()))?;
208        for (key, value) in &self.0 {
209            map.serialize_entry(key, value)?;
210        }
211        map.end()
212    }
213}
214
215impl<'de> serde::Deserialize<'de> for ConfigSettings {
216    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
217        struct Visitor;
218
219        impl<'de> serde::de::Visitor<'de> for Visitor {
220            type Value = ConfigSettings;
221
222            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
223                formatter.write_str("a map from string to string or list of strings")
224            }
225
226            fn visit_map<A: serde::de::MapAccess<'de>>(
227                self,
228                mut map: A,
229            ) -> Result<Self::Value, A::Error> {
230                let mut config = BTreeMap::default();
231                while let Some((key, value)) = map.next_entry()? {
232                    config.insert(key, value);
233                }
234                Ok(ConfigSettings(config))
235            }
236        }
237
238        deserializer.deserialize_map(Visitor)
239    }
240}
241
242/// Settings to pass to PEP 517 build backends on a per-package basis.
243#[derive(Debug, Default, Clone, PartialEq, Eq)]
244#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
245pub struct PackageConfigSettings(BTreeMap<PackageName, ConfigSettings>);
246
247impl FromIterator<ConfigSettingPackageEntry> for PackageConfigSettings {
248    fn from_iter<T: IntoIterator<Item = ConfigSettingPackageEntry>>(iter: T) -> Self {
249        let mut package_configs: BTreeMap<PackageName, Vec<ConfigSettingEntry>> = BTreeMap::new();
250
251        for entry in iter {
252            package_configs
253                .entry(entry.package)
254                .or_default()
255                .push(entry.setting);
256        }
257
258        let configs = package_configs
259            .into_iter()
260            .map(|(package, entries)| (package, entries.into_iter().collect()))
261            .collect();
262
263        Self(configs)
264    }
265}
266
267impl PackageConfigSettings {
268    /// Returns the config settings for a specific package, if any.
269    pub fn get(&self, package: &PackageName) -> Option<&ConfigSettings> {
270        self.0.get(package)
271    }
272
273    /// Merge two sets of package config settings, with the values in `self` taking precedence.
274    #[must_use]
275    pub fn merge(mut self, other: Self) -> Self {
276        for (package, settings) in other.0 {
277            match self.0.entry(package) {
278                Entry::Vacant(vacant) => {
279                    vacant.insert(settings);
280                }
281                Entry::Occupied(mut occupied) => {
282                    let merged = mem::take(occupied.get_mut()).merge(settings);
283                    occupied.insert(merged);
284                }
285            }
286        }
287        self
288    }
289}
290
291impl uv_cache_key::CacheKey for PackageConfigSettings {
292    fn cache_key(&self, state: &mut CacheKeyHasher) {
293        for (package, settings) in &self.0 {
294            package.to_string().cache_key(state);
295            settings.cache_key(state);
296        }
297    }
298}
299
300impl serde::Serialize for PackageConfigSettings {
301    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
302        use serde::ser::SerializeMap;
303
304        let mut map = serializer.serialize_map(Some(self.0.len()))?;
305        for (key, value) in &self.0 {
306            map.serialize_entry(&key.to_string(), value)?;
307        }
308        map.end()
309    }
310}
311
312impl<'de> serde::Deserialize<'de> for PackageConfigSettings {
313    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
314        struct Visitor;
315
316        impl<'de> serde::de::Visitor<'de> for Visitor {
317            type Value = PackageConfigSettings;
318
319            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
320                formatter.write_str("a map from package name to config settings")
321            }
322
323            fn visit_map<A: serde::de::MapAccess<'de>>(
324                self,
325                mut map: A,
326            ) -> Result<Self::Value, A::Error> {
327                let mut config = BTreeMap::default();
328                while let Some((key, value)) = map.next_entry::<String, ConfigSettings>()? {
329                    let package = PackageName::from_str(&key).map_err(|e| {
330                        serde::de::Error::custom(format!("Invalid package name: {e}"))
331                    })?;
332                    config.insert(package, value);
333                }
334                Ok(PackageConfigSettings(config))
335            }
336        }
337
338        deserializer.deserialize_map(Visitor)
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345
346    #[test]
347    fn collect_config_settings() {
348        let settings: ConfigSettings = vec![
349            ConfigSettingEntry {
350                key: "key".to_string(),
351                value: "value".to_string(),
352            },
353            ConfigSettingEntry {
354                key: "key".to_string(),
355                value: "value2".to_string(),
356            },
357            ConfigSettingEntry {
358                key: "list".to_string(),
359                value: "value3".to_string(),
360            },
361            ConfigSettingEntry {
362                key: "list".to_string(),
363                value: "value4".to_string(),
364            },
365        ]
366        .into_iter()
367        .collect();
368        assert_eq!(
369            settings.0.get("key"),
370            Some(&ConfigSettingValue::List(vec![
371                "value".to_string(),
372                "value2".to_string()
373            ]))
374        );
375        assert_eq!(
376            settings.0.get("list"),
377            Some(&ConfigSettingValue::List(vec![
378                "value3".to_string(),
379                "value4".to_string()
380            ]))
381        );
382    }
383
384    #[test]
385    fn escape_for_python() {
386        let mut settings = ConfigSettings::default();
387        settings.0.insert(
388            "key".to_string(),
389            ConfigSettingValue::String("value".to_string()),
390        );
391        settings.0.insert(
392            "list".to_string(),
393            ConfigSettingValue::List(vec!["value1".to_string(), "value2".to_string()]),
394        );
395        assert_eq!(
396            settings.escape_for_python(),
397            r#"{"key":"value","list":["value1","value2"]}"#
398        );
399
400        let mut settings = ConfigSettings::default();
401        settings.0.insert(
402            "key".to_string(),
403            ConfigSettingValue::String("Hello, \"world!\"".to_string()),
404        );
405        settings.0.insert(
406            "list".to_string(),
407            ConfigSettingValue::List(vec!["'value1'".to_string()]),
408        );
409        assert_eq!(
410            settings.escape_for_python(),
411            r#"{"key":"Hello, \"world!\"","list":["'value1'"]}"#
412        );
413
414        let mut settings = ConfigSettings::default();
415        settings.0.insert(
416            "key".to_string(),
417            ConfigSettingValue::String("val\\1 {}value".to_string()),
418        );
419        assert_eq!(settings.escape_for_python(), r#"{"key":"val\\1 {}value"}"#);
420    }
421
422    #[test]
423    fn parse_config_setting_package_entry() {
424        // Test valid parsing
425        let entry = ConfigSettingPackageEntry::from_str("numpy:editable_mode=compat").unwrap();
426        assert_eq!(entry.package.as_ref(), "numpy");
427        assert_eq!(entry.setting.key, "editable_mode");
428        assert_eq!(entry.setting.value, "compat");
429
430        // Test with package name containing hyphens
431        let entry = ConfigSettingPackageEntry::from_str("my-package:some_key=value").unwrap();
432        assert_eq!(entry.package.as_ref(), "my-package");
433        assert_eq!(entry.setting.key, "some_key");
434        assert_eq!(entry.setting.value, "value");
435
436        // Test with spaces around values
437        let entry = ConfigSettingPackageEntry::from_str("  numpy : key = value  ").unwrap();
438        assert_eq!(entry.package.as_ref(), "numpy");
439        assert_eq!(entry.setting.key, "key");
440        assert_eq!(entry.setting.value, "value");
441    }
442
443    #[test]
444    fn collect_config_settings_package() {
445        let settings: PackageConfigSettings = vec![
446            ConfigSettingPackageEntry::from_str("numpy:editable_mode=compat").unwrap(),
447            ConfigSettingPackageEntry::from_str("numpy:another_key=value").unwrap(),
448            ConfigSettingPackageEntry::from_str("scipy:build_option=fast").unwrap(),
449        ]
450        .into_iter()
451        .collect();
452
453        let numpy_settings = settings
454            .get(&PackageName::from_str("numpy").unwrap())
455            .unwrap();
456        assert_eq!(
457            numpy_settings.0.get("editable_mode"),
458            Some(&ConfigSettingValue::String("compat".to_string()))
459        );
460        assert_eq!(
461            numpy_settings.0.get("another_key"),
462            Some(&ConfigSettingValue::String("value".to_string()))
463        );
464
465        let scipy_settings = settings
466            .get(&PackageName::from_str("scipy").unwrap())
467            .unwrap();
468        assert_eq!(
469            scipy_settings.0.get("build_option"),
470            Some(&ConfigSettingValue::String("fast".to_string()))
471        );
472    }
473}