Skip to main content

uv_distribution_types/
specified_requirement.rs

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