uv_configuration/
extras.rs

1use std::{borrow::Cow, sync::Arc};
2
3use uv_normalize::{DefaultExtras, ExtraName};
4
5/// Manager of all extra decisions and settings history.
6///
7/// This is an Arc mostly just to avoid size bloat on things that contain these.
8#[derive(Debug, Default, Clone)]
9pub struct ExtrasSpecification(Arc<ExtrasSpecificationInner>);
10
11/// Manager of all dependency-group decisions and settings history.
12#[derive(Debug, Default, Clone)]
13pub struct ExtrasSpecificationInner {
14    /// Extras to include.
15    include: IncludeExtras,
16    /// Extras to exclude (always wins over include).
17    exclude: Vec<ExtraName>,
18    /// Whether an `--only` flag was passed.
19    ///
20    /// If true, users of this API should refrain from looking at packages
21    /// that *aren't* specified by the extras. This is exposed
22    /// via [`ExtrasSpecificationInner::prod`][].
23    only_extras: bool,
24    /// The "raw" flags/settings we were passed for diagnostics.
25    history: ExtrasSpecificationHistory,
26}
27
28impl ExtrasSpecification {
29    /// Create from history.
30    ///
31    /// This is the "real" constructor, it's basically taking raw CLI flags but in
32    /// a way that's a bit nicer for other constructors to use.
33    fn from_history(history: ExtrasSpecificationHistory) -> Self {
34        let ExtrasSpecificationHistory {
35            mut extra,
36            mut only_extra,
37            no_extra,
38            all_extras,
39            no_default_extras,
40            mut defaults,
41        } = history.clone();
42
43        // `extra` and `only_extra` actually have the same meanings: packages to include.
44        // But if `only_extra` is non-empty then *other* packages should be excluded.
45        // So we just record whether it was and then treat the two lists as equivalent.
46        let only_extras = !only_extra.is_empty();
47        // --only flags imply --no-default-extras
48        let default_extras = !no_default_extras && !only_extras;
49
50        let include = if all_extras {
51            // If this is set we can ignore extra/only_extra/defaults as irrelevant.
52            IncludeExtras::All
53        } else {
54            // Merge all these lists, they're equivalent now
55            extra.append(&mut only_extra);
56            // Resolve default extras potentially also setting All
57            if default_extras {
58                match &mut defaults {
59                    DefaultExtras::All => IncludeExtras::All,
60                    DefaultExtras::List(defaults) => {
61                        extra.append(defaults);
62                        IncludeExtras::Some(extra)
63                    }
64                }
65            } else {
66                IncludeExtras::Some(extra)
67            }
68        };
69
70        Self(Arc::new(ExtrasSpecificationInner {
71            include,
72            exclude: no_extra,
73            only_extras,
74            history,
75        }))
76    }
77
78    /// Create from raw CLI args
79    #[allow(clippy::fn_params_excessive_bools)]
80    pub fn from_args(
81        extra: Vec<ExtraName>,
82        no_extra: Vec<ExtraName>,
83        no_default_extras: bool,
84        only_extra: Vec<ExtraName>,
85        all_extras: bool,
86    ) -> Self {
87        Self::from_history(ExtrasSpecificationHistory {
88            extra,
89            only_extra,
90            no_extra,
91            all_extras,
92            no_default_extras,
93            // This is unknown at CLI-time, use `.with_defaults(...)` to apply this later!
94            defaults: DefaultExtras::default(),
95        })
96    }
97
98    /// Helper to make a spec from just a --extra
99    pub fn from_extra(extra: Vec<ExtraName>) -> Self {
100        Self::from_history(ExtrasSpecificationHistory {
101            extra,
102            ..Default::default()
103        })
104    }
105
106    /// Helper to make a spec from just --all-extras
107    pub fn from_all_extras() -> Self {
108        Self::from_history(ExtrasSpecificationHistory {
109            all_extras: true,
110            ..Default::default()
111        })
112    }
113
114    /// Apply defaults to a base [`ExtrasSpecification`].
115    pub fn with_defaults(&self, defaults: DefaultExtras) -> ExtrasSpecificationWithDefaults {
116        // Explicitly clone the inner history and set the defaults, then remake the result.
117        let mut history = self.0.history.clone();
118        history.defaults = defaults;
119
120        ExtrasSpecificationWithDefaults {
121            cur: Self::from_history(history),
122            prev: self.clone(),
123        }
124    }
125}
126
127impl std::ops::Deref for ExtrasSpecification {
128    type Target = ExtrasSpecificationInner;
129    fn deref(&self) -> &Self::Target {
130        &self.0
131    }
132}
133
134impl ExtrasSpecificationInner {
135    /// Returns `true` if packages other than the ones referenced by these
136    /// extras should be considered.
137    ///
138    /// That is, if I tell you to install a project and this is false,
139    /// you should ignore the project itself and all its dependencies,
140    /// and instead just install the extras.
141    ///
142    /// (This is really just asking if an --only flag was passed.)
143    pub fn prod(&self) -> bool {
144        !self.only_extras
145    }
146
147    /// Returns `true` if the specification includes the given extra.
148    pub fn contains(&self, extra: &ExtraName) -> bool {
149        // exclude always trumps include
150        !self.exclude.contains(extra) && self.include.contains(extra)
151    }
152
153    /// Iterate over all extras that we think should exist.
154    pub fn desugarred_names(&self) -> impl Iterator<Item = &ExtraName> {
155        self.include.names().chain(&self.exclude)
156    }
157
158    /// Returns an iterator over all extras that are included in the specification,
159    /// assuming `all_names` is an iterator over all extras.
160    pub fn extra_names<'a, Names>(
161        &'a self,
162        all_names: Names,
163    ) -> impl Iterator<Item = &'a ExtraName> + 'a
164    where
165        Names: Iterator<Item = &'a ExtraName> + 'a,
166    {
167        all_names.filter(move |name| self.contains(name))
168    }
169
170    /// Iterate over all groups the user explicitly asked for on the CLI
171    pub fn explicit_names(&self) -> impl Iterator<Item = &ExtraName> {
172        let ExtrasSpecificationHistory {
173            extra,
174            only_extra,
175            no_extra,
176            // These reference no extras explicitly
177            all_extras: _,
178            no_default_extras: _,
179            defaults: _,
180        } = self.history();
181
182        extra.iter().chain(no_extra).chain(only_extra)
183    }
184
185    /// Returns `true` if the specification will have no effect.
186    pub fn is_empty(&self) -> bool {
187        self.prod() && self.exclude.is_empty() && self.include.is_empty()
188    }
189
190    /// Get the raw history for diagnostics
191    pub fn history(&self) -> &ExtrasSpecificationHistory {
192        &self.history
193    }
194}
195
196/// Context about a [`ExtrasSpecification`][] that we've preserved for diagnostics
197#[derive(Debug, Default, Clone)]
198pub struct ExtrasSpecificationHistory {
199    pub extra: Vec<ExtraName>,
200    pub only_extra: Vec<ExtraName>,
201    pub no_extra: Vec<ExtraName>,
202    pub all_extras: bool,
203    pub no_default_extras: bool,
204    pub defaults: DefaultExtras,
205}
206
207impl ExtrasSpecificationHistory {
208    /// Returns all the CLI flags that this represents.
209    ///
210    /// If a flag was provided multiple times (e.g. `--extra A --extra B`) this will
211    /// elide the arguments and just show the flag once (e.g. just yield "--extra").
212    ///
213    /// Conceptually this being an empty list should be equivalent to
214    /// [`ExtrasSpecification::is_empty`][] when there aren't any defaults set.
215    /// When there are defaults the two will disagree, and rightfully so!
216    pub fn as_flags_pretty(&self) -> Vec<Cow<'_, str>> {
217        let Self {
218            extra,
219            no_extra,
220            all_extras,
221            only_extra,
222            no_default_extras,
223            // defaults aren't CLI flags!
224            defaults: _,
225        } = self;
226
227        let mut flags = vec![];
228        if *all_extras {
229            flags.push(Cow::Borrowed("--all-extras"));
230        }
231        if *no_default_extras {
232            flags.push(Cow::Borrowed("--no-default-extras"));
233        }
234        match &**extra {
235            [] => {}
236            [extra] => flags.push(Cow::Owned(format!("--extra {extra}"))),
237            [..] => flags.push(Cow::Borrowed("--extra")),
238        }
239        match &**only_extra {
240            [] => {}
241            [extra] => flags.push(Cow::Owned(format!("--only-extra {extra}"))),
242            [..] => flags.push(Cow::Borrowed("--only-extra")),
243        }
244        match &**no_extra {
245            [] => {}
246            [extra] => flags.push(Cow::Owned(format!("--no-extra {extra}"))),
247            [..] => flags.push(Cow::Borrowed("--no-extra")),
248        }
249        flags
250    }
251}
252
253/// A trivial newtype wrapped around [`ExtrasSpecification`][] that signifies "defaults applied"
254///
255/// It includes a copy of the previous semantics to provide info on if
256/// the group being a default actually affected it being enabled, because it's obviously "correct".
257/// (These are Arcs so it's ~free to hold onto the previous semantics)
258#[derive(Debug, Clone)]
259pub struct ExtrasSpecificationWithDefaults {
260    /// The active semantics
261    cur: ExtrasSpecification,
262    /// The semantics before defaults were applied
263    prev: ExtrasSpecification,
264}
265
266impl ExtrasSpecificationWithDefaults {
267    /// Do not enable any extras
268    ///
269    /// Many places in the code need to know what extras are active,
270    /// but various commands or subsystems never enable any extras,
271    /// in which case they want this.
272    pub fn none() -> Self {
273        ExtrasSpecification::default().with_defaults(DefaultExtras::default())
274    }
275    /// Returns `true` if the specification was enabled, and *only* because it was a default
276    pub fn contains_because_default(&self, extra: &ExtraName) -> bool {
277        self.cur.contains(extra) && !self.prev.contains(extra)
278    }
279}
280impl std::ops::Deref for ExtrasSpecificationWithDefaults {
281    type Target = ExtrasSpecification;
282    fn deref(&self) -> &Self::Target {
283        &self.cur
284    }
285}
286
287#[derive(Debug, Clone)]
288pub enum IncludeExtras {
289    /// Include dependencies from the specified extras.
290    Some(Vec<ExtraName>),
291    /// A marker indicates including dependencies from all extras.
292    All,
293}
294
295impl IncludeExtras {
296    /// Returns `true` if the specification includes the given extra.
297    pub fn contains(&self, extra: &ExtraName) -> bool {
298        match self {
299            Self::Some(extras) => extras.contains(extra),
300            Self::All => true,
301        }
302    }
303
304    /// Returns `true` if the specification will have no effect.
305    pub fn is_empty(&self) -> bool {
306        match self {
307            Self::Some(extras) => extras.is_empty(),
308            // Although technically this is a noop if they have no extras,
309            // conceptually they're *trying* to have an effect, so treat it as one.
310            Self::All => false,
311        }
312    }
313
314    /// Iterate over all extras referenced in the [`IncludeExtras`].
315    pub fn names(&self) -> std::slice::Iter<'_, ExtraName> {
316        match self {
317            Self::Some(extras) => extras.iter(),
318            Self::All => [].iter(),
319        }
320    }
321}
322
323impl Default for IncludeExtras {
324    fn default() -> Self {
325        Self::Some(Vec::new())
326    }
327}