1use std::str::FromStr;
2
3use rustc_hash::FxHashMap;
4use tracing::trace;
5
6use uv_distribution_types::{IndexUrl, InstalledDist, InstalledDistKind};
7use uv_normalize::PackageName;
8use uv_pep440::{Operator, Version};
9use uv_pep508::{MarkerTree, VersionOrUrl};
10use uv_pypi_types::{HashDigest, HashDigests, HashError};
11use uv_requirements_txt::{RequirementEntry, RequirementsTxtRequirement};
12
13use crate::ResolverEnvironment;
14use crate::universal_marker::UniversalMarker;
15
16#[derive(thiserror::Error, Debug)]
17pub enum PreferenceError {
18 #[error(transparent)]
19 Hash(#[from] HashError),
20}
21
22#[derive(Clone, Debug)]
24pub struct Preference {
25 name: PackageName,
26 version: Version,
27 marker: MarkerTree,
29 index: PreferenceIndex,
31 fork_markers: Vec<UniversalMarker>,
34 hashes: HashDigests,
35 source: PreferenceSource,
37}
38
39impl Preference {
40 pub fn from_entry(entry: RequirementEntry) -> Result<Option<Self>, PreferenceError> {
42 let RequirementsTxtRequirement::Named(requirement) = entry.requirement else {
43 return Ok(None);
44 };
45
46 let Some(VersionOrUrl::VersionSpecifier(specifier)) = requirement.version_or_url.as_ref()
47 else {
48 trace!("Excluding {requirement} from preferences due to non-version specifier");
49 return Ok(None);
50 };
51
52 let [specifier] = specifier.as_ref() else {
53 trace!("Excluding {requirement} from preferences due to multiple version specifiers");
54 return Ok(None);
55 };
56
57 if *specifier.operator() != Operator::Equal {
58 trace!("Excluding {requirement} from preferences due to inexact version specifier");
59 return Ok(None);
60 }
61
62 Ok(Some(Self {
63 name: requirement.name,
64 version: specifier.version().clone(),
65 marker: requirement.marker,
66 fork_markers: vec![],
68 index: PreferenceIndex::Any,
70 hashes: entry
71 .hashes
72 .iter()
73 .map(String::as_str)
74 .map(HashDigest::from_str)
75 .collect::<Result<_, _>>()?,
76 source: PreferenceSource::RequirementsTxt,
77 }))
78 }
79
80 pub fn from_locked(
82 name: PackageName,
83 version: Version,
84 index: Option<IndexUrl>,
85 fork_markers: Vec<UniversalMarker>,
86 ) -> Self {
87 Self {
88 name,
89 version,
90 marker: MarkerTree::TRUE,
91 index: PreferenceIndex::from(index),
92 fork_markers,
93 hashes: HashDigests::empty(),
94 source: PreferenceSource::Lock,
95 }
96 }
97
98 pub fn from_installed(dist: &InstalledDist) -> Option<Self> {
100 let InstalledDistKind::Registry(dist) = &dist.kind else {
101 return None;
102 };
103 Some(Self {
104 name: dist.name.clone(),
105 version: dist.version.clone(),
106 marker: MarkerTree::TRUE,
107 index: PreferenceIndex::Any,
108 fork_markers: vec![],
109 hashes: HashDigests::empty(),
110 source: PreferenceSource::Environment,
111 })
112 }
113
114 pub fn name(&self) -> &PackageName {
116 &self.name
117 }
118}
119
120#[derive(Debug, Clone)]
121pub(crate) enum PreferenceIndex {
122 Any,
124 Implicit,
126 Explicit(IndexUrl),
128}
129
130impl PreferenceIndex {
131 pub(crate) fn matches(&self, index: &IndexUrl) -> bool {
133 match self {
134 Self::Any => true,
135 Self::Implicit => false,
136 Self::Explicit(preference) => {
137 *preference.url() == *index.without_credentials()
140 }
141 }
142 }
143}
144
145impl From<Option<IndexUrl>> for PreferenceIndex {
146 fn from(index: Option<IndexUrl>) -> Self {
147 match index {
148 Some(index) => Self::Explicit(index),
149 None => Self::Implicit,
150 }
151 }
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub(crate) enum PreferenceSource {
156 Environment,
158 Lock,
160 RequirementsTxt,
162 Resolver,
164}
165
166#[derive(Debug, Clone)]
167pub(crate) struct Entry {
168 marker: UniversalMarker,
169 index: PreferenceIndex,
170 pin: Pin,
171 source: PreferenceSource,
172}
173
174impl Entry {
175 pub(crate) fn marker(&self) -> &UniversalMarker {
177 &self.marker
178 }
179
180 pub(crate) fn index(&self) -> &PreferenceIndex {
182 &self.index
183 }
184
185 pub(crate) fn pin(&self) -> &Pin {
187 &self.pin
188 }
189
190 pub(crate) fn source(&self) -> PreferenceSource {
192 self.source
193 }
194}
195
196#[derive(Debug, Clone, Default)]
203pub struct Preferences(FxHashMap<PackageName, Vec<Entry>>);
204
205impl Preferences {
206 pub fn from_iter(
211 preferences: impl IntoIterator<Item = Preference>,
212 env: &ResolverEnvironment,
213 ) -> Self {
214 let mut map = FxHashMap::<PackageName, Vec<_>>::default();
215 for preference in preferences {
216 if let Some(markers) = env.marker_environment() {
218 if !preference.marker.evaluate(markers, &[]) {
219 trace!("Excluding {preference} from preferences due to unmatched markers");
220 continue;
221 }
222
223 if !preference.fork_markers.is_empty() {
224 if !preference
225 .fork_markers
226 .iter()
227 .any(|marker| marker.evaluate_no_extras(markers))
228 {
229 trace!(
230 "Excluding {preference} from preferences due to unmatched fork markers"
231 );
232 continue;
233 }
234 }
235 }
236
237 if preference.fork_markers.is_empty() {
239 map.entry(preference.name).or_default().push(Entry {
240 marker: UniversalMarker::TRUE,
241 index: preference.index,
242 pin: Pin {
243 version: preference.version,
244 hashes: preference.hashes,
245 },
246 source: preference.source,
247 });
248 } else {
249 for fork_marker in preference.fork_markers {
250 map.entry(preference.name.clone()).or_default().push(Entry {
251 marker: fork_marker,
252 index: preference.index.clone(),
253 pin: Pin {
254 version: preference.version.clone(),
255 hashes: preference.hashes.clone(),
256 },
257 source: preference.source,
258 });
259 }
260 }
261 }
262
263 Self(map)
264 }
265
266 pub(crate) fn insert(
268 &mut self,
269 package_name: PackageName,
270 index: Option<IndexUrl>,
271 markers: UniversalMarker,
272 pin: impl Into<Pin>,
273 source: PreferenceSource,
274 ) {
275 self.0.entry(package_name).or_default().push(Entry {
276 marker: markers,
277 index: PreferenceIndex::from(index),
278 pin: pin.into(),
279 source,
280 });
281 }
282
283 pub(crate) fn iter(
285 &self,
286 ) -> impl Iterator<
287 Item = (
288 &PackageName,
289 impl Iterator<Item = (&UniversalMarker, &PreferenceIndex, &Version)>,
290 ),
291 > {
292 self.0.iter().map(|(name, preferences)| {
293 (
294 name,
295 preferences
296 .iter()
297 .map(|entry| (&entry.marker, &entry.index, entry.pin.version())),
298 )
299 })
300 }
301
302 pub(crate) fn get(&self, package_name: &PackageName) -> &[Entry] {
304 self.0
305 .get(package_name)
306 .map(Vec::as_slice)
307 .unwrap_or_default()
308 }
309
310 pub(crate) fn match_hashes(
312 &self,
313 package_name: &PackageName,
314 version: &Version,
315 ) -> Option<&[HashDigest]> {
316 self.0
317 .get(package_name)
318 .into_iter()
319 .flatten()
320 .find(|entry| entry.pin.version() == version)
321 .map(|entry| entry.pin.hashes())
322 }
323}
324
325impl std::fmt::Display for Preference {
326 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
327 write!(f, "{}=={}", self.name, self.version)
328 }
329}
330
331#[derive(Debug, Clone)]
333pub(crate) struct Pin {
334 version: Version,
335 hashes: HashDigests,
336}
337
338impl Pin {
339 pub(crate) fn version(&self) -> &Version {
341 &self.version
342 }
343
344 fn hashes(&self) -> &[HashDigest] {
346 self.hashes.as_slice()
347 }
348}
349
350impl From<Version> for Pin {
351 fn from(version: Version) -> Self {
352 Self {
353 version,
354 hashes: HashDigests::empty(),
355 }
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362 use std::str::FromStr;
363
364 #[test]
369 fn test_preference_index_matches_ignores_credentials() {
370 let index_without_creds = IndexUrl::from_str("https:/pypi_index.com/simple").unwrap();
372
373 let index_with_username =
375 IndexUrl::from_str("https://username@pypi_index.com/simple").unwrap();
376
377 let preference = PreferenceIndex::Explicit(index_without_creds.clone());
378
379 assert!(
380 preference.matches(&index_with_username),
381 "PreferenceIndex should match URLs that differ only in username"
382 );
383 }
384}