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#[derive(
15 Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, serde::Serialize, serde::Deserialize,
16)]
17pub struct NameRequirementSpecification {
18 #[serde(flatten)]
20 pub requirement: Requirement,
21 #[serde(default, skip_serializing_if = "Vec::is_empty")]
23 pub hashes: Vec<String>,
24}
25
26impl NameRequirementSpecification {
27 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 #[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#[derive(Debug, Clone, Eq, PartialEq, Hash)]
48pub struct UnresolvedRequirementSpecification {
49 pub requirement: UnresolvedRequirement,
51 pub hashes: Vec<String>,
53}
54
55#[derive(Hash, Debug, Clone, Eq, PartialEq)]
63pub enum UnresolvedRequirement {
64 Named(Requirement),
67 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 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 #[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 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 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}