uv_configuration/
excludes.rs1use std::str::FromStr;
2
3use rustc_hash::{FxHashMap, FxHashSet};
4use serde::de::Error;
5
6use uv_normalize::PackageName;
7use uv_pep440::Version;
8
9use crate::Overrides;
10
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
13#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
14#[serde(rename_all = "kebab-case", deny_unknown_fields)]
15pub struct PackageExclusion {
16 pub package: PackageExclusionTarget,
17 pub dependencies: Box<[PackageName]>,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
22#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
23#[serde(rename_all = "kebab-case", deny_unknown_fields)]
24pub struct PackageExclusionTarget {
25 pub name: PackageName,
26 #[cfg_attr(
27 feature = "schemars",
28 schemars(
29 with = "Option<String>",
30 description = "PEP 440-style package version, e.g., `1.2.3`"
31 )
32 )]
33 pub version: Option<Version>,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
38#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema), schemars(untagged))]
39#[serde(untagged)]
40pub enum ExcludeDependency {
41 Package(PackageExclusion),
42 Dependency(PackageName),
43}
44
45impl<'de> serde::Deserialize<'de> for ExcludeDependency {
46 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
47 where
48 D: serde::Deserializer<'de>,
49 {
50 serde_untagged::UntaggedEnumVisitor::new()
51 .string(|string| {
52 PackageName::from_str(string)
53 .map(Self::Dependency)
54 .map_err(Error::custom)
55 })
56 .map(|map| map.deserialize().map(Self::Package))
57 .deserialize(deserializer)
58 }
59}
60
61#[derive(Debug, Default, Clone)]
63pub struct Excludes {
64 global: FxHashSet<PackageName>,
65 scoped: FxHashMap<PackageName, Vec<ScopedExclusions>>,
66}
67
68#[derive(Debug, Clone)]
69struct ScopedExclusions {
70 version: Option<Version>,
71 excludes: FxHashSet<PackageName>,
72}
73
74impl Excludes {
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 fn contains(&self, name: &PackageName) -> bool {
104 self.global.contains(name)
105 }
106
107 pub fn contains_for(
109 &self,
110 package: &PackageName,
111 version: &Version,
112 dependency: &PackageName,
113 ) -> bool {
114 self.contains_for_package(Some((package, version)), dependency)
115 }
116
117 pub fn contains_for_scope(
122 &self,
123 overrides: &Overrides,
124 package: &PackageName,
125 version: Option<&Version>,
126 dependency: &PackageName,
127 ) -> bool {
128 if let Some(version) = version {
129 return self.contains_for(package, version, dependency);
130 }
131 if self.contains(dependency) {
132 return true;
133 }
134
135 let Some(entries) = self.scoped.get(package) else {
136 return false;
137 };
138 entries
139 .iter()
140 .find(|entry| entry.version.is_none())
141 .is_some_and(|entry| entry.excludes.contains(dependency))
142 && entries
143 .iter()
144 .filter(|entry| {
145 entry
146 .version
147 .as_ref()
148 .is_some_and(|version| !overrides.has_exact_scope(package, version))
149 })
150 .all(|entry| entry.excludes.contains(dependency))
151 }
152
153 pub fn contains_for_package(
155 &self,
156 package: Option<(&PackageName, &Version)>,
157 dependency: &PackageName,
158 ) -> bool {
159 self.contains(dependency)
160 || package.is_some_and(|(package, version)| {
161 self.scoped.get(package).is_some_and(|entries| {
162 entries
163 .iter()
164 .find(|entry| entry.version.as_ref() == Some(version))
165 .or_else(|| entries.iter().find(|entry| entry.version.is_none()))
166 .is_some_and(|entry| entry.excludes.contains(dependency))
167 })
168 })
169 }
170}
171
172impl FromIterator<PackageName> for Excludes {
173 fn from_iter<I: IntoIterator<Item = PackageName>>(iter: I) -> Self {
174 Self::from_entries(iter.into_iter().map(ExcludeDependency::Dependency))
175 }
176}