Skip to main content

uv_distribution_types/
specified_requirement.rs

1use std::borrow::Cow;
2use std::fmt::{Display, Formatter};
3use std::io;
4use std::path::Path;
5
6use uv_git_types::{GitLfs, GitReference};
7use uv_normalize::ExtraName;
8use uv_pep508::{MarkerEnvironment, MarkerTree, UnnamedRequirement};
9use uv_pypi_types::{HashError, Hashes, ParsedUrl};
10
11use crate::{Requirement, RequirementSource, VerbatimParsedUrl};
12
13/// A named requirement with hashes from `requirements.txt` or `pyproject.toml`.
14#[derive(
15    Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, serde::Serialize, serde::Deserialize,
16)]
17pub struct NameRequirementSpecification {
18    /// The actual requirement.
19    #[serde(flatten)]
20    pub requirement: Requirement,
21    /// Hashes of the downloadable packages.
22    #[serde(default, skip_serializing_if = "Vec::is_empty")]
23    pub hashes: Vec<String>,
24}
25
26impl NameRequirementSpecification {
27    /// Make the requirement's path relative to the given root.
28    pub fn relative_to(self, root: &Path) -> Result<Self, io::Error> {
29        Ok(Self {
30            requirement: self.requirement.relative_to(root)?,
31            hashes: self.hashes,
32        })
33    }
34
35    /// Make the requirement's path absolute based on the given root.
36    #[must_use]
37    pub fn into_absolute(self, root: &Path) -> Self {
38        Self {
39            requirement: self.requirement.into_absolute(root),
40            hashes: self.hashes,
41        }
42    }
43}
44
45/// An [`UnresolvedRequirement`] with additional metadata from `requirements.txt`, currently only
46/// hashes but in the future also editable and similar information.
47#[derive(Debug, Clone, Eq, PartialEq, Hash)]
48pub struct UnresolvedRequirementSpecification {
49    /// The actual requirement.
50    pub requirement: UnresolvedRequirement,
51    /// Hashes of the downloadable packages.
52    pub hashes: Vec<String>,
53}
54
55/// A requirement read from a `requirements.txt` or `pyproject.toml` file.
56///
57/// It is considered unresolved as we still need to query the URL for the `Unnamed` variant to
58/// resolve the requirement name.
59///
60/// Analog to `RequirementsTxtRequirement` but with `distribution_types::Requirement` instead of
61/// `uv_pep508::Requirement`.
62#[derive(Hash, Debug, Clone, Eq, PartialEq)]
63pub enum UnresolvedRequirement {
64    /// The uv-specific superset over PEP 508 requirements specifier incorporating
65    /// `tool.uv.sources`.
66    Named(Requirement),
67    /// A PEP 508-like, direct URL dependency specifier.
68    Unnamed(UnnamedRequirement<VerbatimParsedUrl>),
69}
70
71impl Display for UnresolvedRequirement {
72    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
73        match self {
74            Self::Named(requirement) => write!(f, "{requirement}"),
75            Self::Unnamed(requirement) => write!(f, "{requirement}"),
76        }
77    }
78}
79
80impl UnresolvedRequirement {
81    /// Returns whether the markers apply for the given environment.
82    ///
83    /// When the environment is not given, this treats all marker expressions
84    /// that reference the environment as true. In other words, it does
85    /// environment independent expression evaluation. (Which in turn devolves
86    /// to "only evaluate marker expressions that reference an extra name.")
87    pub fn evaluate_markers(&self, env: Option<&MarkerEnvironment>, extras: &[ExtraName]) -> bool {
88        match self {
89            Self::Named(requirement) => requirement.evaluate_markers(env, extras),
90            Self::Unnamed(requirement) => requirement.evaluate_optional_environment(env, extras),
91        }
92    }
93
94    /// Augment a user-provided requirement by attaching any specification data that was provided
95    /// separately from the requirement itself (e.g., `--branch main`).
96    #[must_use]
97    pub fn augment_requirement(
98        self,
99        rev: Option<&str>,
100        tag: Option<&str>,
101        branch: Option<&str>,
102        lfs: Option<bool>,
103        marker: Option<MarkerTree>,
104    ) -> Self {
105        #[expect(clippy::manual_map)]
106        let git_reference = if let Some(rev) = rev {
107            Some(GitReference::from_rev(rev.to_string()))
108        } else if let Some(tag) = tag {
109            Some(GitReference::Tag(tag.to_string()))
110        } else if let Some(branch) = branch {
111            Some(GitReference::Branch(branch.to_string()))
112        } else {
113            None
114        };
115
116        match self {
117            Self::Named(mut requirement) => Self::Named(Requirement {
118                marker: marker
119                    .map(|marker| {
120                        requirement.marker = requirement.marker.and(marker);
121                        requirement.marker
122                    })
123                    .unwrap_or(requirement.marker),
124                source: match requirement.source {
125                    RequirementSource::GitDirectory {
126                        git,
127                        subdirectory,
128                        url,
129                    } => {
130                        let git = if let Some(git_reference) = git_reference {
131                            git.with_reference(git_reference)
132                        } else {
133                            git
134                        };
135                        let git = if let Some(lfs) = lfs {
136                            git.with_lfs(GitLfs::from(lfs))
137                        } else {
138                            git
139                        };
140                        RequirementSource::GitDirectory {
141                            git,
142                            subdirectory,
143                            url,
144                        }
145                    }
146                    RequirementSource::GitPath {
147                        git,
148                        install_path,
149                        ext,
150                        url,
151                    } => {
152                        let git = if let Some(git_reference) = git_reference {
153                            git.with_reference(git_reference)
154                        } else {
155                            git
156                        };
157                        let git = if let Some(lfs) = lfs {
158                            git.with_lfs(GitLfs::from(lfs))
159                        } else {
160                            git
161                        };
162                        RequirementSource::GitPath {
163                            git,
164                            install_path,
165                            ext,
166                            url,
167                        }
168                    }
169                    _ => requirement.source,
170                },
171                ..requirement
172            }),
173            Self::Unnamed(mut requirement) => Self::Unnamed(UnnamedRequirement {
174                marker: marker
175                    .map(|marker| {
176                        requirement.marker = requirement.marker.and(marker);
177                        requirement.marker
178                    })
179                    .unwrap_or(requirement.marker),
180                url: match requirement.url.parsed_url {
181                    ParsedUrl::GitDirectory(mut git) => {
182                        if let Some(git_reference) = git_reference {
183                            git.url = git.url.with_reference(git_reference);
184                        }
185                        if let Some(lfs) = lfs {
186                            git.url = git.url.with_lfs(GitLfs::from(lfs));
187                        }
188                        VerbatimParsedUrl {
189                            parsed_url: ParsedUrl::GitDirectory(git),
190                            verbatim: requirement.url.verbatim,
191                        }
192                    }
193                    ParsedUrl::GitPath(mut git) => {
194                        if let Some(git_reference) = git_reference {
195                            git.url = git.url.with_reference(git_reference);
196                        }
197                        if let Some(lfs) = lfs {
198                            git.url = git.url.with_lfs(GitLfs::from(lfs));
199                        }
200                        VerbatimParsedUrl {
201                            parsed_url: ParsedUrl::GitPath(git),
202                            verbatim: requirement.url.verbatim,
203                        }
204                    }
205                    _ => requirement.url,
206                },
207                ..requirement
208            }),
209        }
210    }
211
212    /// Return the version specifier or URL for the requirement.
213    pub fn source(&self) -> Cow<'_, RequirementSource> {
214        match self {
215            Self::Named(requirement) => Cow::Borrowed(&requirement.source),
216            Self::Unnamed(requirement) => Cow::Owned(RequirementSource::from_parsed_url(
217                requirement.url.parsed_url.clone(),
218                requirement.url.verbatim.clone(),
219            )),
220        }
221    }
222
223    /// Return the hashes of the requirement, as specified in the URL fragment.
224    pub fn hashes(&self) -> Result<Option<Hashes>, HashError> {
225        match self {
226            Self::Named(requirement) => requirement.hashes(),
227            Self::Unnamed(requirement) => {
228                let Some(fragment) = requirement.url.verbatim.fragment() else {
229                    return Ok(None);
230                };
231                Hashes::parse_url_fragment(fragment)
232            }
233        }
234    }
235}
236
237impl From<Requirement> for UnresolvedRequirementSpecification {
238    fn from(requirement: Requirement) -> Self {
239        Self {
240            requirement: UnresolvedRequirement::Named(requirement),
241            hashes: Vec::new(),
242        }
243    }
244}
245
246impl From<Requirement> for NameRequirementSpecification {
247    fn from(requirement: Requirement) -> Self {
248        Self {
249            requirement,
250            hashes: Vec::new(),
251        }
252    }
253}