uv_configuration/
excludes.rs1use 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#[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#[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 pub fn package(&self) -> &PackageName {
38 &self.package.name
39 }
40}
41
42#[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#[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 #[must_use]
70 pub(crate) fn with_recorder(mut self, recorder: Option<ResolutionRecorder>) -> Self {
71 self.recorder = recorder;
72 self
73 }
74
75 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 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 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 pub(crate) fn has_scoped_package(&self, package: &PackageName) -> bool {
138 self.scoped.contains_key(package)
139 }
140
141 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 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 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}