uv_distribution_types/
config_settings.rs

1use std::{
2    collections::{BTreeMap, btree_map::Entry},
3    str::FromStr,
4};
5use uv_cache_key::CacheKeyHasher;
6use uv_normalize::PackageName;
7
8#[derive(Debug, Clone)]
9pub struct ConfigSettingEntry {
10    /// The key of the setting. For example, given `key=value`, this would be `key`.
11    key: String,
12    /// The value of the setting. For example, given `key=value`, this would be `value`.
13    value: String,
14}
15
16impl FromStr for ConfigSettingEntry {
17    type Err = String;
18
19    fn from_str(s: &str) -> Result<Self, Self::Err> {
20        let Some((key, value)) = s.split_once('=') else {
21            return Err(format!(
22                "Invalid config setting: {s} (expected `KEY=VALUE`)"
23            ));
24        };
25        Ok(Self {
26            key: key.trim().to_string(),
27            value: value.trim().to_string(),
28        })
29    }
30}
31
32#[derive(Debug, Clone)]
33pub struct ConfigSettingPackageEntry {
34    /// The package name to apply the setting to.
35    package: PackageName,
36    /// The config setting entry.
37    setting: ConfigSettingEntry,
38}
39
40impl FromStr for ConfigSettingPackageEntry {
41    type Err = String;
42
43    fn from_str(s: &str) -> Result<Self, Self::Err> {
44        let Some((package_str, config_str)) = s.split_once(':') else {
45            return Err(format!(
46                "Invalid config setting: {s} (expected `PACKAGE:KEY=VALUE`)"
47            ));
48        };
49
50        let package = PackageName::from_str(package_str.trim())
51            .map_err(|e| format!("Invalid package name: {e}"))?;
52        let setting = ConfigSettingEntry::from_str(config_str)?;
53
54        Ok(Self { package, setting })
55    }
56}
57
58#[derive(Debug, Clone, Hash, PartialEq, Eq)]
59#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema), schemars(untagged))]
60enum ConfigSettingValue {
61    /// The value consists of a single string.
62    String(String),
63    /// The value consists of a list of strings.
64    List(Vec<String>),
65}
66
67impl serde::Serialize for ConfigSettingValue {
68    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
69        match self {
70            Self::String(value) => serializer.serialize_str(value),
71            Self::List(values) => serializer.collect_seq(values.iter()),
72        }
73    }
74}
75
76impl<'de> serde::Deserialize<'de> for ConfigSettingValue {
77    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
78        struct Visitor;
79
80        impl<'de> serde::de::Visitor<'de> for Visitor {
81            type Value = ConfigSettingValue;
82
83            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
84                formatter.write_str("a string or list of strings")
85            }
86
87            fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
88                Ok(ConfigSettingValue::String(value.to_string()))
89            }
90
91            fn visit_seq<A: serde::de::SeqAccess<'de>>(
92                self,
93                mut seq: A,
94            ) -> Result<Self::Value, A::Error> {
95                let mut values = Vec::new();
96                while let Some(value) = seq.next_element()? {
97                    values.push(value);
98                }
99                Ok(ConfigSettingValue::List(values))
100            }
101        }
102
103        deserializer.deserialize_any(Visitor)
104    }
105}
106
107/// Settings to pass to a PEP 517 build backend, structured as a map from (string) key to string or
108/// list of strings.
109///
110/// See: <https://peps.python.org/pep-0517/#config-settings>
111#[derive(Debug, Default, Hash, Clone, PartialEq, Eq)]
112#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
113pub struct ConfigSettings(BTreeMap<String, ConfigSettingValue>);
114
115impl FromIterator<ConfigSettingEntry> for ConfigSettings {
116    fn from_iter<T: IntoIterator<Item = ConfigSettingEntry>>(iter: T) -> Self {
117        let mut config = BTreeMap::default();
118        for entry in iter {
119            match config.entry(entry.key) {
120                Entry::Vacant(vacant) => {
121                    vacant.insert(ConfigSettingValue::String(entry.value));
122                }
123                Entry::Occupied(mut occupied) => match occupied.get_mut() {
124                    ConfigSettingValue::String(existing) => {
125                        let existing = existing.clone();
126                        occupied.insert(ConfigSettingValue::List(vec![existing, entry.value]));
127                    }
128                    ConfigSettingValue::List(existing) => {
129                        existing.push(entry.value);
130                    }
131                },
132            }
133        }
134        Self(config)
135    }
136}
137
138impl ConfigSettings {
139    /// Returns the number of settings in the configuration.
140    pub fn len(&self) -> usize {
141        self.0.len()
142    }
143
144    /// Returns `true` if the configuration contains no settings.
145    pub 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 = existing.clone();
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    /// Returns `true` if there are no package-specific settings.
274    pub fn is_empty(&self) -> bool {
275        self.0.is_empty()
276    }
277
278    /// Merge two sets of package config settings, with the values in `self` taking precedence.
279    #[must_use]
280    pub fn merge(mut self, other: Self) -> Self {
281        for (package, settings) in other.0 {
282            match self.0.entry(package) {
283                Entry::Vacant(vacant) => {
284                    vacant.insert(settings);
285                }
286                Entry::Occupied(mut occupied) => {
287                    let merged = occupied.get().clone().merge(settings);
288                    occupied.insert(merged);
289                }
290            }
291        }
292        self
293    }
294}
295
296impl uv_cache_key::CacheKey for PackageConfigSettings {
297    fn cache_key(&self, state: &mut CacheKeyHasher) {
298        for (package, settings) in &self.0 {
299            package.to_string().cache_key(state);
300            settings.cache_key(state);
301        }
302    }
303}
304
305impl serde::Serialize for PackageConfigSettings {
306    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
307        use serde::ser::SerializeMap;
308
309        let mut map = serializer.serialize_map(Some(self.0.len()))?;
310        for (key, value) in &self.0 {
311            map.serialize_entry(&key.to_string(), value)?;
312        }
313        map.end()
314    }
315}
316
317impl<'de> serde::Deserialize<'de> for PackageConfigSettings {
318    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
319        struct Visitor;
320
321        impl<'de> serde::de::Visitor<'de> for Visitor {
322            type Value = PackageConfigSettings;
323
324            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
325                formatter.write_str("a map from package name to config settings")
326            }
327
328            fn visit_map<A: serde::de::MapAccess<'de>>(
329                self,
330                mut map: A,
331            ) -> Result<Self::Value, A::Error> {
332                let mut config = BTreeMap::default();
333                while let Some((key, value)) = map.next_entry::<String, ConfigSettings>()? {
334                    let package = PackageName::from_str(&key).map_err(|e| {
335                        serde::de::Error::custom(format!("Invalid package name: {e}"))
336                    })?;
337                    config.insert(package, value);
338                }
339                Ok(PackageConfigSettings(config))
340            }
341        }
342
343        deserializer.deserialize_map(Visitor)
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    #[test]
352    fn collect_config_settings() {
353        let settings: ConfigSettings = vec![
354            ConfigSettingEntry {
355                key: "key".to_string(),
356                value: "value".to_string(),
357            },
358            ConfigSettingEntry {
359                key: "key".to_string(),
360                value: "value2".to_string(),
361            },
362            ConfigSettingEntry {
363                key: "list".to_string(),
364                value: "value3".to_string(),
365            },
366            ConfigSettingEntry {
367                key: "list".to_string(),
368                value: "value4".to_string(),
369            },
370        ]
371        .into_iter()
372        .collect();
373        assert_eq!(
374            settings.0.get("key"),
375            Some(&ConfigSettingValue::List(vec![
376                "value".to_string(),
377                "value2".to_string()
378            ]))
379        );
380        assert_eq!(
381            settings.0.get("list"),
382            Some(&ConfigSettingValue::List(vec![
383                "value3".to_string(),
384                "value4".to_string()
385            ]))
386        );
387    }
388
389    #[test]
390    fn escape_for_python() {
391        let mut settings = ConfigSettings::default();
392        settings.0.insert(
393            "key".to_string(),
394            ConfigSettingValue::String("value".to_string()),
395        );
396        settings.0.insert(
397            "list".to_string(),
398            ConfigSettingValue::List(vec!["value1".to_string(), "value2".to_string()]),
399        );
400        assert_eq!(
401            settings.escape_for_python(),
402            r#"{"key":"value","list":["value1","value2"]}"#
403        );
404
405        let mut settings = ConfigSettings::default();
406        settings.0.insert(
407            "key".to_string(),
408            ConfigSettingValue::String("Hello, \"world!\"".to_string()),
409        );
410        settings.0.insert(
411            "list".to_string(),
412            ConfigSettingValue::List(vec!["'value1'".to_string()]),
413        );
414        assert_eq!(
415            settings.escape_for_python(),
416            r#"{"key":"Hello, \"world!\"","list":["'value1'"]}"#
417        );
418
419        let mut settings = ConfigSettings::default();
420        settings.0.insert(
421            "key".to_string(),
422            ConfigSettingValue::String("val\\1 {}value".to_string()),
423        );
424        assert_eq!(settings.escape_for_python(), r#"{"key":"val\\1 {}value"}"#);
425    }
426
427    #[test]
428    fn parse_config_setting_package_entry() {
429        // Test valid parsing
430        let entry = ConfigSettingPackageEntry::from_str("numpy:editable_mode=compat").unwrap();
431        assert_eq!(entry.package.as_ref(), "numpy");
432        assert_eq!(entry.setting.key, "editable_mode");
433        assert_eq!(entry.setting.value, "compat");
434
435        // Test with package name containing hyphens
436        let entry = ConfigSettingPackageEntry::from_str("my-package:some_key=value").unwrap();
437        assert_eq!(entry.package.as_ref(), "my-package");
438        assert_eq!(entry.setting.key, "some_key");
439        assert_eq!(entry.setting.value, "value");
440
441        // Test with spaces around values
442        let entry = ConfigSettingPackageEntry::from_str("  numpy : key = value  ").unwrap();
443        assert_eq!(entry.package.as_ref(), "numpy");
444        assert_eq!(entry.setting.key, "key");
445        assert_eq!(entry.setting.value, "value");
446    }
447
448    #[test]
449    fn collect_config_settings_package() {
450        let settings: PackageConfigSettings = vec![
451            ConfigSettingPackageEntry::from_str("numpy:editable_mode=compat").unwrap(),
452            ConfigSettingPackageEntry::from_str("numpy:another_key=value").unwrap(),
453            ConfigSettingPackageEntry::from_str("scipy:build_option=fast").unwrap(),
454        ]
455        .into_iter()
456        .collect();
457
458        let numpy_settings = settings
459            .get(&PackageName::from_str("numpy").unwrap())
460            .unwrap();
461        assert_eq!(
462            numpy_settings.0.get("editable_mode"),
463            Some(&ConfigSettingValue::String("compat".to_string()))
464        );
465        assert_eq!(
466            numpy_settings.0.get("another_key"),
467            Some(&ConfigSettingValue::String("value".to_string()))
468        );
469
470        let scipy_settings = settings
471            .get(&PackageName::from_str("scipy").unwrap())
472            .unwrap();
473        assert_eq!(
474            scipy_settings.0.get("build_option"),
475            Some(&ConfigSettingValue::String("fast".to_string()))
476        );
477    }
478}