uv_settings/
combine.rs

1use std::path::PathBuf;
2use std::{collections::BTreeMap, num::NonZeroUsize};
3
4use url::Url;
5
6use uv_configuration::{
7    BuildIsolation, ExportFormat, IndexStrategy, KeyringProviderType, Reinstall, RequiredVersion,
8    TargetTriple, TrustedPublishing, Upgrade,
9};
10use uv_distribution_types::{
11    ConfigSettings, ExtraBuildVariables, Index, IndexUrl, PackageConfigSettings, PipExtraIndex,
12    PipFindLinks, PipIndex,
13};
14use uv_install_wheel::LinkMode;
15use uv_pypi_types::{SchemaConflicts, SupportedEnvironments};
16use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
17use uv_redacted::DisplaySafeUrl;
18use uv_resolver::{
19    AnnotationStyle, ExcludeNewer, ExcludeNewerPackage, ExcludeNewerValue, ForkStrategy,
20    PrereleaseMode, ResolutionMode,
21};
22use uv_torch::TorchMode;
23use uv_workspace::pyproject::ExtraBuildDependencies;
24use uv_workspace::pyproject_mut::AddBoundsKind;
25
26use crate::{FilesystemOptions, Options, PipOptions};
27
28pub trait Combine {
29    /// Combine two values, preferring the values in `self`.
30    ///
31    /// The logic should follow that of Cargo's `config.toml`:
32    ///
33    /// > If a key is specified in multiple config files, the values will get merged together.
34    /// > Numbers, strings, and booleans will use the value in the deeper config directory taking
35    /// > precedence over ancestor directories, where the home directory is the lowest priority.
36    /// > Arrays will be joined together with higher precedence items being placed later in the
37    /// > merged array.
38    ///
39    /// ...with one exception: we place items with higher precedence earlier in the merged array.
40    #[must_use]
41    fn combine(self, other: Self) -> Self;
42}
43
44impl Combine for Option<FilesystemOptions> {
45    /// Combine the options used in two [`FilesystemOptions`]s. Retains the root of `self`.
46    fn combine(self, other: Self) -> Self {
47        match (self, other) {
48            (Some(a), Some(b)) => Some(FilesystemOptions(
49                a.into_options().combine(b.into_options()),
50            )),
51            (a, b) => a.or(b),
52        }
53    }
54}
55
56impl Combine for Option<Options> {
57    /// Combine the options used in two [`Options`]s. Retains the root of `self`.
58    fn combine(self, other: Self) -> Self {
59        match (self, other) {
60            (Some(a), Some(b)) => Some(a.combine(b)),
61            (a, b) => a.or(b),
62        }
63    }
64}
65
66impl Combine for Option<PipOptions> {
67    fn combine(self, other: Self) -> Self {
68        match (self, other) {
69            (Some(a), Some(b)) => Some(a.combine(b)),
70            (a, b) => a.or(b),
71        }
72    }
73}
74
75macro_rules! impl_combine_or {
76    ($name:ident) => {
77        impl Combine for Option<$name> {
78            fn combine(self, other: Option<$name>) -> Option<$name> {
79                self.or(other)
80            }
81        }
82    };
83}
84
85impl_combine_or!(AddBoundsKind);
86impl_combine_or!(AnnotationStyle);
87impl_combine_or!(ExcludeNewer);
88impl_combine_or!(ExcludeNewerValue);
89impl_combine_or!(ExportFormat);
90impl_combine_or!(ForkStrategy);
91impl_combine_or!(Index);
92impl_combine_or!(IndexStrategy);
93impl_combine_or!(IndexUrl);
94impl_combine_or!(KeyringProviderType);
95impl_combine_or!(LinkMode);
96impl_combine_or!(DisplaySafeUrl);
97impl_combine_or!(NonZeroUsize);
98impl_combine_or!(PathBuf);
99impl_combine_or!(PipExtraIndex);
100impl_combine_or!(PipFindLinks);
101impl_combine_or!(PipIndex);
102impl_combine_or!(PrereleaseMode);
103impl_combine_or!(PythonDownloads);
104impl_combine_or!(PythonPreference);
105impl_combine_or!(PythonVersion);
106impl_combine_or!(RequiredVersion);
107impl_combine_or!(ResolutionMode);
108impl_combine_or!(SchemaConflicts);
109impl_combine_or!(String);
110impl_combine_or!(SupportedEnvironments);
111impl_combine_or!(TargetTriple);
112impl_combine_or!(TorchMode);
113impl_combine_or!(TrustedPublishing);
114impl_combine_or!(Url);
115impl_combine_or!(bool);
116
117impl<T> Combine for Option<Vec<T>> {
118    /// Combine two vectors by extending the vector in `self` with the vector in `other`, if they're
119    /// both `Some`.
120    fn combine(self, other: Self) -> Self {
121        match (self, other) {
122            (Some(mut a), Some(b)) => {
123                a.extend(b);
124                Some(a)
125            }
126            (a, b) => a.or(b),
127        }
128    }
129}
130
131impl<K: Ord, T> Combine for Option<BTreeMap<K, Vec<T>>> {
132    /// Combine two maps of vecs by combining their vecs
133    fn combine(self, other: Self) -> Self {
134        match (self, other) {
135            (Some(mut a), Some(b)) => {
136                for (key, value) in b {
137                    a.entry(key).or_default().extend(value);
138                }
139                Some(a)
140            }
141            (a, b) => a.or(b),
142        }
143    }
144}
145
146impl Combine for Option<ExcludeNewerPackage> {
147    /// Combine two [`ExcludeNewerPackage`] instances by merging them, with the values in `self` taking precedence.
148    fn combine(self, other: Self) -> Self {
149        match (self, other) {
150            (Some(mut a), Some(b)) => {
151                // Extend with values from b, but a takes precedence (we don't overwrite existing keys)
152                for (key, value) in b {
153                    a.entry(key).or_insert(value);
154                }
155                Some(a)
156            }
157            (a, b) => a.or(b),
158        }
159    }
160}
161
162impl Combine for Option<ConfigSettings> {
163    /// Combine two maps by merging the map in `self` with the map in `other`, if they're both
164    /// `Some`.
165    fn combine(self, other: Self) -> Self {
166        match (self, other) {
167            (Some(a), Some(b)) => Some(a.merge(b)),
168            (a, b) => a.or(b),
169        }
170    }
171}
172
173impl Combine for Option<PackageConfigSettings> {
174    /// Combine two maps by merging the map in `self` with the map in `other`, if they're both
175    /// `Some`.
176    fn combine(self, other: Self) -> Self {
177        match (self, other) {
178            (Some(a), Some(b)) => Some(a.merge(b)),
179            (a, b) => a.or(b),
180        }
181    }
182}
183
184impl Combine for Option<Upgrade> {
185    fn combine(self, other: Self) -> Self {
186        match (self, other) {
187            (Some(a), Some(b)) => Some(a.combine(b)),
188            (a, b) => a.or(b),
189        }
190    }
191}
192
193impl Combine for Option<Reinstall> {
194    fn combine(self, other: Self) -> Self {
195        match (self, other) {
196            (Some(a), Some(b)) => Some(a.combine(b)),
197            (a, b) => a.or(b),
198        }
199    }
200}
201
202impl Combine for Option<BuildIsolation> {
203    fn combine(self, other: Self) -> Self {
204        match (self, other) {
205            (Some(a), Some(b)) => Some(a.combine(b)),
206            (a, b) => a.or(b),
207        }
208    }
209}
210
211impl Combine for serde::de::IgnoredAny {
212    fn combine(self, _other: Self) -> Self {
213        self
214    }
215}
216
217impl Combine for Option<serde::de::IgnoredAny> {
218    fn combine(self, _other: Self) -> Self {
219        self
220    }
221}
222
223impl Combine for ExcludeNewer {
224    fn combine(mut self, other: Self) -> Self {
225        self.global = self.global.combine(other.global);
226
227        if !other.package.is_empty() {
228            if self.package.is_empty() {
229                self.package = other.package;
230            } else {
231                // Merge package-specific timestamps, with self taking precedence
232                for (pkg, timestamp) in &other.package {
233                    self.package.entry(pkg.clone()).or_insert(timestamp.clone());
234                }
235            }
236        }
237
238        self
239    }
240}
241
242impl Combine for ExtraBuildDependencies {
243    fn combine(mut self, other: Self) -> Self {
244        for (key, value) in other {
245            match self.entry(key) {
246                std::collections::btree_map::Entry::Occupied(mut entry) => {
247                    // Combine the vecs, with self taking precedence
248                    let existing = entry.get_mut();
249                    existing.extend(value);
250                }
251                std::collections::btree_map::Entry::Vacant(entry) => {
252                    entry.insert(value);
253                }
254            }
255        }
256        self
257    }
258}
259
260impl Combine for Option<ExtraBuildDependencies> {
261    fn combine(self, other: Self) -> Self {
262        match (self, other) {
263            (Some(a), Some(b)) => Some(a.combine(b)),
264            (a, b) => a.or(b),
265        }
266    }
267}
268
269impl Combine for ExtraBuildVariables {
270    fn combine(mut self, other: Self) -> Self {
271        for (key, value) in other {
272            match self.entry(key) {
273                std::collections::btree_map::Entry::Occupied(mut entry) => {
274                    // Combine the maps, with self taking precedence
275                    let existing = entry.get_mut();
276                    for (k, v) in value {
277                        existing.entry(k).or_insert(v);
278                    }
279                }
280                std::collections::btree_map::Entry::Vacant(entry) => {
281                    entry.insert(value);
282                }
283            }
284        }
285        self
286    }
287}
288
289impl Combine for Option<ExtraBuildVariables> {
290    fn combine(self, other: Self) -> Self {
291        match (self, other) {
292            (Some(a), Some(b)) => Some(a.combine(b)),
293            (a, b) => a.or(b),
294        }
295    }
296}