Skip to main content

uv_configuration/
excludes.rs

1use rustc_hash::{FxHashMap, FxHashSet};
2
3use uv_distribution_types::ResolutionRecorder;
4use uv_macros::DeserializeUntagged;
5use uv_normalize::PackageName;
6use uv_pep440::Version;
7
8use crate::Overrides;
9
10/// A set of exclusions that applies to the dependencies of a specific package version.
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
12#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
13#[serde(rename_all = "kebab-case", deny_unknown_fields)]
14pub struct PackageExclusion {
15    package: PackageExclusionTarget,
16    dependencies: Box<[PackageName]>,
17}
18
19/// The package and optional version selected by a [`PackageExclusion`].
20#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
21#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
22#[serde(rename_all = "kebab-case", deny_unknown_fields)]
23pub struct PackageExclusionTarget {
24    name: PackageName,
25    #[cfg_attr(
26        feature = "schemars",
27        schemars(
28            with = "Option<String>",
29            description = "PEP 440-style package version, e.g., `1.2.3`"
30        )
31    )]
32    version: Option<Version>,
33}
34
35impl PackageExclusion {
36    /// Return the parent package selected by this exclusion.
37    pub fn package(&self) -> &PackageName {
38        &self.package.name
39    }
40}
41
42/// An exclusion, either global or scoped to a specific package version.
43#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, DeserializeUntagged)]
44#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema), schemars(untagged))]
45#[serde(untagged, expecting = "a string or map")]
46pub enum ExcludeDependency {
47    #[untagged(map)]
48    Package(PackageExclusion),
49    #[untagged(string)]
50    Dependency(PackageName),
51}
52
53/// A set of packages to exclude from resolution.
54#[derive(Debug, Default, Clone)]
55pub struct Excludes {
56    recorder: Option<ResolutionRecorder>,
57    global: FxHashSet<PackageName>,
58    scoped: FxHashMap<PackageName, Vec<ScopedExclusions>>,
59}
60
61#[derive(Debug, Clone)]
62struct ScopedExclusions {
63    version: Option<Version>,
64    excludes: FxHashSet<PackageName>,
65}
66
67impl Excludes {
68    /// Record which settings are consulted while resolving runtime dependencies.
69    #[must_use]
70    pub(crate) fn with_recorder(mut self, recorder: Option<ResolutionRecorder>) -> Self {
71        self.recorder = recorder;
72        self
73    }
74
75    /// Create an indexed set of exclusions.
76    pub fn from_entries(entries: impl IntoIterator<Item = ExcludeDependency>) -> Self {
77        let mut excludes = Self::default();
78        for entry in entries {
79            match entry {
80                ExcludeDependency::Dependency(dependency) => {
81                    excludes.global.insert(dependency);
82                }
83                ExcludeDependency::Package(package) => {
84                    let packages = excludes.scoped.entry(package.package.name).or_default();
85                    if let Some(entry) = packages
86                        .iter_mut()
87                        .find(|entry| entry.version == package.package.version)
88                    {
89                        entry.excludes.extend(package.dependencies);
90                    } else {
91                        packages.push(ScopedExclusions {
92                            version: package.package.version,
93                            excludes: package.dependencies.into_iter().collect(),
94                        });
95                    }
96                }
97            }
98        }
99        excludes
100    }
101
102    /// Return sorted declarations, combining duplicate exclusions within each package scope.
103    ///
104    /// Retain empty version-specific scopes: they shadow exclusions from a versionless scope.
105    pub(crate) fn into_entries(self) -> Vec<ExcludeDependency> {
106        let mut entries = self
107            .global
108            .into_iter()
109            .map(ExcludeDependency::Dependency)
110            .collect::<Vec<_>>();
111        for (name, packages) in self.scoped {
112            for package in packages {
113                let mut dependencies = package.excludes.into_iter().collect::<Vec<_>>();
114                dependencies.sort();
115                entries.push(ExcludeDependency::Package(PackageExclusion {
116                    package: PackageExclusionTarget {
117                        name: name.clone(),
118                        version: package.version,
119                    },
120                    dependencies: dependencies.into_boxed_slice(),
121                }));
122            }
123        }
124        entries.sort();
125        entries
126    }
127
128    /// Check if a package is excluded.
129    pub(crate) fn contains(&self, name: &PackageName) -> bool {
130        if let Some(recorder) = &self.recorder {
131            recorder.exclusion(name);
132        }
133        self.global.contains(name)
134    }
135
136    /// Return whether any exclusions are scoped to the given package.
137    pub(crate) fn has_scoped_package(&self, package: &PackageName) -> bool {
138        self.scoped.contains_key(package)
139    }
140
141    /// Check if a dependency is excluded from a specific package version.
142    pub(crate) fn contains_for(
143        &self,
144        package: &PackageName,
145        version: &Version,
146        dependency: &PackageName,
147    ) -> bool {
148        self.contains_for_package(Some((package, version)), dependency)
149    }
150
151    /// Check if a dependency is always excluded from a package scope.
152    ///
153    /// A versionless scope remains eligible if any exact-version exclusion allows the dependency
154    /// at a version where the override is not shadowed by an exact override scope.
155    pub(crate) fn contains_for_scope(
156        &self,
157        overrides: &Overrides,
158        package: &PackageName,
159        version: Option<&Version>,
160        dependency: &PackageName,
161    ) -> bool {
162        if let Some(version) = version {
163            return self.contains_for(package, version, dependency);
164        }
165        if self.contains(dependency) {
166            return true;
167        }
168
169        let Some(entries) = self.scoped.get(package) else {
170            return false;
171        };
172        entries
173            .iter()
174            .find(|entry| entry.version.is_none())
175            .is_some_and(|entry| entry.excludes.contains(dependency))
176            && entries
177                .iter()
178                .filter(|entry| {
179                    entry
180                        .version
181                        .as_ref()
182                        .is_some_and(|version| !overrides.has_exact_scope(package, version))
183                })
184                .all(|entry| entry.excludes.contains(dependency))
185    }
186
187    /// Check if a dependency is excluded with optional package-version context.
188    pub(crate) fn contains_for_package(
189        &self,
190        package: Option<(&PackageName, &Version)>,
191        dependency: &PackageName,
192    ) -> bool {
193        self.contains(dependency)
194            || package.is_some_and(|(package, version)| {
195                if let Some(recorder) = &self.recorder {
196                    recorder.scoped_exclusion(package);
197                }
198                self.scoped.get(package).is_some_and(|entries| {
199                    entries
200                        .iter()
201                        .find(|entry| entry.version.as_ref() == Some(version))
202                        .or_else(|| entries.iter().find(|entry| entry.version.is_none()))
203                        .is_some_and(|entry| entry.excludes.contains(dependency))
204                })
205            })
206    }
207}
208
209impl FromIterator<PackageName> for Excludes {
210    fn from_iter<I: IntoIterator<Item = PackageName>>(iter: I) -> Self {
211        Self::from_entries(iter.into_iter().map(ExcludeDependency::Dependency))
212    }
213}