Skip to main content

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    pub fn from_args(
80        extra: Vec<ExtraName>,
81        no_extra: Vec<ExtraName>,
82        no_default_extras: bool,
83        only_extra: Vec<ExtraName>,
84        all_extras: bool,
85    ) -> Self {
86        Self::from_history(ExtrasSpecificationHistory {
87            extra,
88            only_extra,
89            no_extra,
90            all_extras,
91            no_default_extras,
92            // This is unknown at CLI-time, use `.with_defaults(...)` to apply this later!
93            defaults: DefaultExtras::default(),
94        })
95    }
96
97    /// Helper to make a spec from just a --extra
98    pub fn from_extra(extra: Vec<ExtraName>) -> Self {
99        Self::from_history(ExtrasSpecificationHistory {
100            extra,
101            ..Default::default()
102        })
103    }
104
105    /// Helper to make a spec from just --all-extras
106    pub fn from_all_extras() -> Self {
107        Self::from_history(ExtrasSpecificationHistory {
108            all_extras: true,
109            ..Default::default()
110        })
111    }
112
113    /// Apply defaults to a base [`ExtrasSpecification`].
114    pub fn with_defaults(&self, defaults: DefaultExtras) -> ExtrasSpecificationWithDefaults {
115        // Explicitly clone the inner history and set the defaults, then remake the result.
116        let mut history = self.0.history.clone();
117        history.defaults = defaults;
118
119        ExtrasSpecificationWithDefaults {
120            cur: Self::from_history(history),
121        }
122    }
123}
124
125impl std::ops::Deref for ExtrasSpecification {
126    type Target = ExtrasSpecificationInner;
127    fn deref(&self) -> &Self::Target {
128        &self.0
129    }
130}
131
132impl ExtrasSpecificationInner {
133    /// Returns `true` if packages other than the ones referenced by these
134    /// extras should be considered.
135    ///
136    /// That is, if I tell you to install a project and this is false,
137    /// you should ignore the project itself and all its dependencies,
138    /// and instead just install the extras.
139    ///
140    /// (This is really just asking if an --only flag was passed.)
141    fn prod(&self) -> bool {
142        !self.only_extras
143    }
144
145    /// Returns `true` if the specification includes the given extra.
146    pub fn contains(&self, extra: &ExtraName) -> bool {
147        // exclude always trumps include
148        !self.exclude.contains(extra) && self.include.contains(extra)
149    }
150
151    /// Returns an iterator over all extras that are included in the specification,
152    /// assuming `all_names` is an iterator over all extras.
153    pub fn extra_names<'a, Names>(
154        &'a self,
155        all_names: Names,
156    ) -> impl Iterator<Item = &'a ExtraName> + 'a
157    where
158        Names: Iterator<Item = &'a ExtraName> + 'a,
159    {
160        all_names.filter(move |name| self.contains(name))
161    }
162
163    /// Iterate over all groups the user explicitly asked for on the CLI
164    pub fn explicit_names(&self) -> impl Iterator<Item = &ExtraName> {
165        let ExtrasSpecificationHistory {
166            extra,
167            only_extra,
168            no_extra,
169            // These reference no extras explicitly
170            all_extras: _,
171            no_default_extras: _,
172            defaults: _,
173        } = self.history();
174
175        extra.iter().chain(no_extra).chain(only_extra)
176    }
177
178    /// Returns `true` if the specification will have no effect.
179    pub fn is_empty(&self) -> bool {
180        self.prod() && self.exclude.is_empty() && self.include.is_empty()
181    }
182
183    /// Get the raw history for diagnostics
184    pub fn history(&self) -> &ExtrasSpecificationHistory {
185        &self.history
186    }
187}
188
189/// Context about a [`ExtrasSpecification`][] that we've preserved for diagnostics
190#[derive(Debug, Default, Clone)]
191pub struct ExtrasSpecificationHistory {
192    extra: Vec<ExtraName>,
193    only_extra: Vec<ExtraName>,
194    no_extra: Vec<ExtraName>,
195    all_extras: bool,
196    no_default_extras: bool,
197    defaults: DefaultExtras,
198}
199
200impl ExtrasSpecificationHistory {
201    /// Returns all the CLI flags that this represents.
202    ///
203    /// If a flag was provided multiple times (e.g. `--extra A --extra B`) this will
204    /// elide the arguments and just show the flag once (e.g. just yield "--extra").
205    ///
206    /// Conceptually this being an empty list should be equivalent to
207    /// [`ExtrasSpecification::is_empty`][] when there aren't any defaults set.
208    /// When there are defaults the two will disagree, and rightfully so!
209    pub fn as_flags_pretty(&self) -> Vec<Cow<'_, str>> {
210        let Self {
211            extra,
212            no_extra,
213            all_extras,
214            only_extra,
215            no_default_extras,
216            // defaults aren't CLI flags!
217            defaults: _,
218        } = self;
219
220        let mut flags = vec![];
221        if *all_extras {
222            flags.push(Cow::Borrowed("--all-extras"));
223        }
224        if *no_default_extras {
225            flags.push(Cow::Borrowed("--no-default-extras"));
226        }
227        match &**extra {
228            [] => {}
229            [extra] => flags.push(Cow::Owned(format!("--extra {extra}"))),
230            [..] => flags.push(Cow::Borrowed("--extra")),
231        }
232        match &**only_extra {
233            [] => {}
234            [extra] => flags.push(Cow::Owned(format!("--only-extra {extra}"))),
235            [..] => flags.push(Cow::Borrowed("--only-extra")),
236        }
237        match &**no_extra {
238            [] => {}
239            [extra] => flags.push(Cow::Owned(format!("--no-extra {extra}"))),
240            [..] => flags.push(Cow::Borrowed("--no-extra")),
241        }
242        flags
243    }
244}
245
246/// A trivial newtype wrapped around [`ExtrasSpecification`][] that signifies "defaults applied"
247#[derive(Debug, Clone)]
248pub struct ExtrasSpecificationWithDefaults {
249    /// The active semantics
250    cur: ExtrasSpecification,
251}
252
253impl std::ops::Deref for ExtrasSpecificationWithDefaults {
254    type Target = ExtrasSpecification;
255    fn deref(&self) -> &Self::Target {
256        &self.cur
257    }
258}
259
260#[derive(Debug, Clone)]
261pub enum IncludeExtras {
262    /// Include dependencies from the specified extras.
263    Some(Vec<ExtraName>),
264    /// A marker indicates including dependencies from all extras.
265    All,
266}
267
268impl IncludeExtras {
269    /// Returns `true` if the specification includes the given extra.
270    fn contains(&self, extra: &ExtraName) -> bool {
271        match self {
272            Self::Some(extras) => extras.contains(extra),
273            Self::All => true,
274        }
275    }
276
277    /// Returns `true` if the specification will have no effect.
278    fn is_empty(&self) -> bool {
279        match self {
280            Self::Some(extras) => extras.is_empty(),
281            // Although technically this is a noop if they have no extras,
282            // conceptually they're *trying* to have an effect, so treat it as one.
283            Self::All => false,
284        }
285    }
286}
287
288impl Default for IncludeExtras {
289    fn default() -> Self {
290        Self::Some(Vec::new())
291    }
292}