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        #[allow(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::Git {
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::Git {
117                            git,
118                            subdirectory,
119                            url,
120                        }
121                    }
122                    _ => requirement.source,
123                },
124                ..requirement
125            }),
126            Self::Unnamed(mut requirement) => Self::Unnamed(UnnamedRequirement {
127                marker: marker
128                    .map(|marker| {
129                        requirement.marker.and(marker);
130                        requirement.marker
131                    })
132                    .unwrap_or(requirement.marker),
133                url: match requirement.url.parsed_url {
134                    ParsedUrl::Git(mut git) => {
135                        if let Some(git_reference) = git_reference {
136                            git.url = git.url.with_reference(git_reference);
137                        }
138                        if let Some(lfs) = lfs {
139                            git.url = git.url.with_lfs(GitLfs::from(lfs));
140                        }
141                        VerbatimParsedUrl {
142                            parsed_url: ParsedUrl::Git(git),
143                            verbatim: requirement.url.verbatim,
144                        }
145                    }
146                    _ => requirement.url,
147                },
148                ..requirement
149            }),
150        }
151    }
152
153    /// Returns the extras for the requirement.
154    pub fn extras(&self) -> &[ExtraName] {
155        match self {
156            Self::Named(requirement) => &requirement.extras,
157            Self::Unnamed(requirement) => &requirement.extras,
158        }
159    }
160
161    /// Return the version specifier or URL for the requirement.
162    pub fn source(&self) -> Cow<'_, RequirementSource> {
163        match self {
164            Self::Named(requirement) => Cow::Borrowed(&requirement.source),
165            Self::Unnamed(requirement) => Cow::Owned(RequirementSource::from_parsed_url(
166                requirement.url.parsed_url.clone(),
167                requirement.url.verbatim.clone(),
168            )),
169        }
170    }
171
172    /// Returns `true` if the requirement is editable.
173    pub fn is_editable(&self) -> bool {
174        match self {
175            Self::Named(requirement) => requirement.is_editable(),
176            Self::Unnamed(requirement) => requirement.url.is_editable(),
177        }
178    }
179
180    /// Return the hashes of the requirement, as specified in the URL fragment.
181    pub fn hashes(&self) -> Option<Hashes> {
182        match self {
183            Self::Named(requirement) => requirement.hashes(),
184            Self::Unnamed(requirement) => {
185                let fragment = requirement.url.verbatim.fragment()?;
186                Hashes::parse_fragment(fragment).ok()
187            }
188        }
189    }
190}
191
192impl NameRequirementSpecification {
193    /// Return the hashes of the requirement, as specified in the URL fragment.
194    pub fn hashes(&self) -> Option<Hashes> {
195        let RequirementSource::Url { ref url, .. } = self.requirement.source else {
196            return None;
197        };
198        let fragment = url.fragment()?;
199        Hashes::parse_fragment(fragment).ok()
200    }
201}
202
203impl From<Requirement> for UnresolvedRequirementSpecification {
204    fn from(requirement: Requirement) -> Self {
205        Self {
206            requirement: UnresolvedRequirement::Named(requirement),
207            hashes: Vec::new(),
208        }
209    }
210}
211
212impl From<Requirement> for NameRequirementSpecification {
213    fn from(requirement: Requirement) -> Self {
214        Self {
215            requirement,
216            hashes: Vec::new(),
217        }
218    }
219}