uv_distribution_types/
requirement.rs

1use std::fmt::{Display, Formatter};
2use std::io;
3use std::path::Path;
4use std::str::FromStr;
5
6use thiserror::Error;
7use uv_cache_key::{CacheKey, CacheKeyHasher};
8use uv_distribution_filename::DistExtension;
9use uv_fs::{CWD, PortablePath, PortablePathBuf, relative_to};
10use uv_git_types::{GitLfs, GitOid, GitReference, GitUrl, GitUrlParseError, OidParseError};
11use uv_normalize::{ExtraName, GroupName, PackageName};
12use uv_pep440::VersionSpecifiers;
13use uv_pep508::{
14    MarkerEnvironment, MarkerTree, RequirementOrigin, VerbatimUrl, VersionOrUrl, marker,
15};
16use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlError};
17
18use crate::{IndexMetadata, IndexUrl};
19
20use uv_pypi_types::{
21    ConflictItem, Hashes, ParsedArchiveUrl, ParsedDirectoryUrl, ParsedGitUrl, ParsedPathUrl,
22    ParsedUrl, ParsedUrlError, VerbatimParsedUrl,
23};
24
25#[derive(Debug, Error)]
26pub enum RequirementError {
27    #[error(transparent)]
28    VerbatimUrlError(#[from] uv_pep508::VerbatimUrlError),
29    #[error(transparent)]
30    ParsedUrlError(#[from] ParsedUrlError),
31    #[error(transparent)]
32    UrlParseError(#[from] DisplaySafeUrlError),
33    #[error(transparent)]
34    OidParseError(#[from] OidParseError),
35    #[error(transparent)]
36    GitUrlParse(#[from] GitUrlParseError),
37}
38
39/// A representation of dependency on a package, an extension over a PEP 508's requirement.
40///
41/// The main change is using [`RequirementSource`] to represent all supported package sources over
42/// [`VersionOrUrl`], which collapses all URL sources into a single stringly type.
43///
44/// Additionally, this requirement type makes room for dependency groups, which lack a standardized
45/// representation in PEP 508. In the context of this type, extras and groups are assumed to be
46/// mutually exclusive, in that if `extras` is non-empty, `groups` must be empty and vice versa.
47#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
48pub struct Requirement {
49    pub name: PackageName,
50    #[serde(skip_serializing_if = "<[ExtraName]>::is_empty", default)]
51    pub extras: Box<[ExtraName]>,
52    #[serde(skip_serializing_if = "<[GroupName]>::is_empty", default)]
53    pub groups: Box<[GroupName]>,
54    #[serde(
55        skip_serializing_if = "marker::ser::is_empty",
56        serialize_with = "marker::ser::serialize",
57        default
58    )]
59    pub marker: MarkerTree,
60    #[serde(flatten)]
61    pub source: RequirementSource,
62    #[serde(skip)]
63    pub origin: Option<RequirementOrigin>,
64}
65
66impl Requirement {
67    /// Returns whether the markers apply for the given environment.
68    ///
69    /// When `env` is `None`, this specifically evaluates all marker
70    /// expressions based on the environment to `true`. That is, this provides
71    /// environment independent marker evaluation.
72    pub fn evaluate_markers(&self, env: Option<&MarkerEnvironment>, extras: &[ExtraName]) -> bool {
73        self.marker.evaluate_optional_environment(env, extras)
74    }
75
76    /// Returns `true` if the requirement is editable.
77    pub fn is_editable(&self) -> bool {
78        self.source.is_editable()
79    }
80
81    /// Convert to a [`Requirement`] with a relative path based on the given root.
82    pub fn relative_to(self, path: &Path) -> Result<Self, io::Error> {
83        Ok(Self {
84            source: self.source.relative_to(path)?,
85            ..self
86        })
87    }
88
89    /// Convert to a [`Requirement`] with an absolute path based on the given root.
90    #[must_use]
91    pub fn to_absolute(self, path: &Path) -> Self {
92        Self {
93            source: self.source.to_absolute(path),
94            ..self
95        }
96    }
97
98    /// Return the hashes of the requirement, as specified in the URL fragment.
99    pub fn hashes(&self) -> Option<Hashes> {
100        let RequirementSource::Url { ref url, .. } = self.source else {
101            return None;
102        };
103        let fragment = url.fragment()?;
104        Hashes::parse_fragment(fragment).ok()
105    }
106
107    /// Set the source file containing the requirement.
108    #[must_use]
109    pub fn with_origin(self, origin: RequirementOrigin) -> Self {
110        Self {
111            origin: Some(origin),
112            ..self
113        }
114    }
115}
116
117impl std::hash::Hash for Requirement {
118    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
119        let Self {
120            name,
121            extras,
122            groups,
123            marker,
124            source,
125            origin: _,
126        } = self;
127        name.hash(state);
128        extras.hash(state);
129        groups.hash(state);
130        marker.hash(state);
131        source.hash(state);
132    }
133}
134
135impl PartialEq for Requirement {
136    fn eq(&self, other: &Self) -> bool {
137        let Self {
138            name,
139            extras,
140            groups,
141            marker,
142            source,
143            origin: _,
144        } = self;
145        let Self {
146            name: other_name,
147            extras: other_extras,
148            groups: other_groups,
149            marker: other_marker,
150            source: other_source,
151            origin: _,
152        } = other;
153        name == other_name
154            && extras == other_extras
155            && groups == other_groups
156            && marker == other_marker
157            && source == other_source
158    }
159}
160
161impl Eq for Requirement {}
162
163impl Ord for Requirement {
164    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
165        let Self {
166            name,
167            extras,
168            groups,
169            marker,
170            source,
171            origin: _,
172        } = self;
173        let Self {
174            name: other_name,
175            extras: other_extras,
176            groups: other_groups,
177            marker: other_marker,
178            source: other_source,
179            origin: _,
180        } = other;
181        name.cmp(other_name)
182            .then_with(|| extras.cmp(other_extras))
183            .then_with(|| groups.cmp(other_groups))
184            .then_with(|| marker.cmp(other_marker))
185            .then_with(|| source.cmp(other_source))
186    }
187}
188
189impl PartialOrd for Requirement {
190    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
191        Some(self.cmp(other))
192    }
193}
194
195impl From<Requirement> for uv_pep508::Requirement<VerbatimUrl> {
196    /// Convert a [`Requirement`] to a [`uv_pep508::Requirement`].
197    fn from(requirement: Requirement) -> Self {
198        Self {
199            name: requirement.name,
200            extras: requirement.extras,
201            marker: requirement.marker,
202            origin: requirement.origin,
203            version_or_url: match requirement.source {
204                RequirementSource::Registry { specifier, .. } => {
205                    Some(VersionOrUrl::VersionSpecifier(specifier))
206                }
207                RequirementSource::Url { url, .. }
208                | RequirementSource::Git { url, .. }
209                | RequirementSource::Path { url, .. }
210                | RequirementSource::Directory { url, .. } => Some(VersionOrUrl::Url(url)),
211            },
212        }
213    }
214}
215
216impl From<Requirement> for uv_pep508::Requirement<VerbatimParsedUrl> {
217    /// Convert a [`Requirement`] to a [`uv_pep508::Requirement`].
218    fn from(requirement: Requirement) -> Self {
219        Self {
220            name: requirement.name,
221            extras: requirement.extras,
222            marker: requirement.marker,
223            origin: requirement.origin,
224            version_or_url: match requirement.source {
225                RequirementSource::Registry { specifier, .. } => {
226                    Some(VersionOrUrl::VersionSpecifier(specifier))
227                }
228                RequirementSource::Url {
229                    location,
230                    subdirectory,
231                    ext,
232                    url,
233                } => Some(VersionOrUrl::Url(VerbatimParsedUrl {
234                    parsed_url: ParsedUrl::Archive(ParsedArchiveUrl {
235                        url: location,
236                        subdirectory,
237                        ext,
238                    }),
239                    verbatim: url,
240                })),
241                RequirementSource::Git {
242                    git,
243                    subdirectory,
244                    url,
245                } => Some(VersionOrUrl::Url(VerbatimParsedUrl {
246                    parsed_url: ParsedUrl::Git(ParsedGitUrl {
247                        url: git,
248                        subdirectory,
249                    }),
250                    verbatim: url,
251                })),
252                RequirementSource::Path {
253                    install_path,
254                    ext,
255                    url,
256                } => Some(VersionOrUrl::Url(VerbatimParsedUrl {
257                    parsed_url: ParsedUrl::Path(ParsedPathUrl {
258                        url: url.to_url(),
259                        install_path,
260                        ext,
261                    }),
262                    verbatim: url,
263                })),
264                RequirementSource::Directory {
265                    install_path,
266                    editable,
267                    r#virtual,
268                    url,
269                } => Some(VersionOrUrl::Url(VerbatimParsedUrl {
270                    parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl {
271                        url: url.to_url(),
272                        install_path,
273                        editable,
274                        r#virtual,
275                    }),
276                    verbatim: url,
277                })),
278            },
279        }
280    }
281}
282
283impl From<uv_pep508::Requirement<VerbatimParsedUrl>> for Requirement {
284    /// Convert a [`uv_pep508::Requirement`] to a [`Requirement`].
285    fn from(requirement: uv_pep508::Requirement<VerbatimParsedUrl>) -> Self {
286        let source = match requirement.version_or_url {
287            None => RequirementSource::Registry {
288                specifier: VersionSpecifiers::empty(),
289                index: None,
290                conflict: None,
291            },
292            // The most popular case: just a name, a version range and maybe extras.
293            Some(VersionOrUrl::VersionSpecifier(specifier)) => RequirementSource::Registry {
294                specifier,
295                index: None,
296                conflict: None,
297            },
298            Some(VersionOrUrl::Url(url)) => {
299                RequirementSource::from_parsed_url(url.parsed_url, url.verbatim)
300            }
301        };
302        Self {
303            name: requirement.name,
304            groups: Box::new([]),
305            extras: requirement.extras,
306            marker: requirement.marker,
307            source,
308            origin: requirement.origin,
309        }
310    }
311}
312
313impl Display for Requirement {
314    /// Display the [`Requirement`], with the intention of being shown directly to a user, rather
315    /// than for inclusion in a `requirements.txt` file.
316    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
317        write!(f, "{}", self.name)?;
318        if !self.extras.is_empty() {
319            write!(
320                f,
321                "[{}]",
322                self.extras
323                    .iter()
324                    .map(ToString::to_string)
325                    .collect::<Vec<_>>()
326                    .join(",")
327            )?;
328        }
329        match &self.source {
330            RequirementSource::Registry {
331                specifier, index, ..
332            } => {
333                write!(f, "{specifier}")?;
334                if let Some(index) = index {
335                    write!(f, " (index: {})", index.url)?;
336                }
337            }
338            RequirementSource::Url { url, .. } => {
339                write!(f, " @ {url}")?;
340            }
341            RequirementSource::Git {
342                url: _,
343                git,
344                subdirectory,
345            } => {
346                write!(f, " @ git+{}", git.repository())?;
347                if let Some(reference) = git.reference().as_str() {
348                    write!(f, "@{reference}")?;
349                }
350                if let Some(subdirectory) = subdirectory {
351                    writeln!(f, "#subdirectory={}", subdirectory.display())?;
352                }
353                if git.lfs().enabled() {
354                    writeln!(
355                        f,
356                        "{}lfs=true",
357                        if subdirectory.is_some() { "&" } else { "#" }
358                    )?;
359                }
360            }
361            RequirementSource::Path { url, .. } => {
362                write!(f, " @ {url}")?;
363            }
364            RequirementSource::Directory { url, .. } => {
365                write!(f, " @ {url}")?;
366            }
367        }
368        if let Some(marker) = self.marker.contents() {
369            write!(f, " ; {marker}")?;
370        }
371        Ok(())
372    }
373}
374
375impl CacheKey for Requirement {
376    fn cache_key(&self, state: &mut CacheKeyHasher) {
377        self.name.as_str().cache_key(state);
378
379        self.groups.len().cache_key(state);
380        for group in &self.groups {
381            group.as_str().cache_key(state);
382        }
383
384        self.extras.len().cache_key(state);
385        for extra in &self.extras {
386            extra.as_str().cache_key(state);
387        }
388
389        if let Some(marker) = self.marker.contents() {
390            1u8.cache_key(state);
391            marker.to_string().cache_key(state);
392        } else {
393            0u8.cache_key(state);
394        }
395
396        match &self.source {
397            RequirementSource::Registry {
398                specifier,
399                index,
400                conflict: _,
401            } => {
402                0u8.cache_key(state);
403                specifier.len().cache_key(state);
404                for spec in specifier.iter() {
405                    spec.operator().as_str().cache_key(state);
406                    spec.version().cache_key(state);
407                }
408                if let Some(index) = index {
409                    1u8.cache_key(state);
410                    index.url.cache_key(state);
411                } else {
412                    0u8.cache_key(state);
413                }
414                // `conflict` is intentionally omitted
415            }
416            RequirementSource::Url {
417                location,
418                subdirectory,
419                ext,
420                url,
421            } => {
422                1u8.cache_key(state);
423                location.cache_key(state);
424                if let Some(subdirectory) = subdirectory {
425                    1u8.cache_key(state);
426                    subdirectory.display().to_string().cache_key(state);
427                } else {
428                    0u8.cache_key(state);
429                }
430                ext.name().cache_key(state);
431                url.cache_key(state);
432            }
433            RequirementSource::Git {
434                git,
435                subdirectory,
436                url,
437            } => {
438                2u8.cache_key(state);
439                git.to_string().cache_key(state);
440                if let Some(subdirectory) = subdirectory {
441                    1u8.cache_key(state);
442                    subdirectory.display().to_string().cache_key(state);
443                } else {
444                    0u8.cache_key(state);
445                }
446                if git.lfs().enabled() {
447                    1u8.cache_key(state);
448                }
449                url.cache_key(state);
450            }
451            RequirementSource::Path {
452                install_path,
453                ext,
454                url,
455            } => {
456                3u8.cache_key(state);
457                install_path.cache_key(state);
458                ext.name().cache_key(state);
459                url.cache_key(state);
460            }
461            RequirementSource::Directory {
462                install_path,
463                editable,
464                r#virtual,
465                url,
466            } => {
467                4u8.cache_key(state);
468                install_path.cache_key(state);
469                editable.cache_key(state);
470                r#virtual.cache_key(state);
471                url.cache_key(state);
472            }
473        }
474
475        // `origin` is intentionally omitted
476    }
477}
478
479/// The different locations with can install a distribution from: Version specifier (from an index),
480/// HTTP(S) URL, git repository, and path.
481///
482/// We store both the parsed fields (such as the plain url and the subdirectory) and the joined
483/// PEP 508 style url (e.g. `file:///<path>#subdirectory=<subdirectory>`) since we need both in
484/// different locations.
485#[derive(
486    Hash, Debug, Clone, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, serde::Deserialize,
487)]
488#[serde(try_from = "RequirementSourceWire", into = "RequirementSourceWire")]
489pub enum RequirementSource {
490    /// The requirement has a version specifier, such as `foo >1,<2`.
491    Registry {
492        specifier: VersionSpecifiers,
493        /// Choose a version from the index at the given URL.
494        index: Option<IndexMetadata>,
495        /// The conflict item associated with the source, if any.
496        conflict: Option<ConflictItem>,
497    },
498    // TODO(konsti): Track and verify version specifier from `project.dependencies` matches the
499    // version in remote location.
500    /// A remote `http://` or `https://` URL, either a built distribution,
501    /// e.g. `foo @ https://example.org/foo-1.0-py3-none-any.whl`, or a source distribution,
502    /// e.g.`foo @ https://example.org/foo-1.0.zip`.
503    Url {
504        /// The remote location of the archive file, without subdirectory fragment.
505        location: DisplaySafeUrl,
506        /// For source distributions, the path to the distribution if it is not in the archive
507        /// root.
508        subdirectory: Option<Box<Path>>,
509        /// The file extension, e.g. `tar.gz`, `zip`, etc.
510        ext: DistExtension,
511        /// The PEP 508 style URL in the format
512        /// `<scheme>://<domain>/<path>#subdirectory=<subdirectory>`.
513        url: VerbatimUrl,
514    },
515    /// A remote Git repository, over either HTTPS or SSH.
516    Git {
517        /// The repository URL and reference to the commit to use.
518        git: GitUrl,
519        /// The path to the source distribution if it is not in the repository root.
520        subdirectory: Option<Box<Path>>,
521        /// The PEP 508 style url in the format
522        /// `git+<scheme>://<domain>/<path>@<rev>#subdirectory=<subdirectory>`.
523        url: VerbatimUrl,
524    },
525    /// A local built or source distribution, either from a path or a `file://` URL. It can either
526    /// be a binary distribution (a `.whl` file) or a source distribution archive (a `.zip` or
527    /// `.tar.gz` file).
528    Path {
529        /// The absolute path to the distribution which we use for installing.
530        install_path: Box<Path>,
531        /// The file extension, e.g. `tar.gz`, `zip`, etc.
532        ext: DistExtension,
533        /// The PEP 508 style URL in the format
534        /// `file:///<path>#subdirectory=<subdirectory>`.
535        url: VerbatimUrl,
536    },
537    /// A local source tree (a directory with a pyproject.toml in, or a legacy
538    /// source distribution with only a setup.py but non pyproject.toml in it).
539    Directory {
540        /// The absolute path to the distribution which we use for installing.
541        install_path: Box<Path>,
542        /// For a source tree (a directory), whether to install as an editable.
543        editable: Option<bool>,
544        /// For a source tree (a directory), whether the project should be built and installed.
545        r#virtual: Option<bool>,
546        /// The PEP 508 style URL in the format
547        /// `file:///<path>#subdirectory=<subdirectory>`.
548        url: VerbatimUrl,
549    },
550}
551
552impl RequirementSource {
553    /// Construct a [`RequirementSource`] for a URL source, given a URL parsed into components and
554    /// the PEP 508 string (after the `@`) as [`VerbatimUrl`].
555    pub fn from_parsed_url(parsed_url: ParsedUrl, url: VerbatimUrl) -> Self {
556        match parsed_url {
557            ParsedUrl::Path(local_file) => Self::Path {
558                install_path: local_file.install_path.clone(),
559                ext: local_file.ext,
560                url,
561            },
562            ParsedUrl::Directory(directory) => Self::Directory {
563                install_path: directory.install_path.clone(),
564                editable: directory.editable,
565                r#virtual: directory.r#virtual,
566                url,
567            },
568            ParsedUrl::Git(git) => Self::Git {
569                git: git.url.clone(),
570                url,
571                subdirectory: git.subdirectory,
572            },
573            ParsedUrl::Archive(archive) => Self::Url {
574                url,
575                location: archive.url,
576                subdirectory: archive.subdirectory,
577                ext: archive.ext,
578            },
579        }
580    }
581
582    /// Convert the source to a [`VerbatimParsedUrl`], if it's a URL source.
583    pub fn to_verbatim_parsed_url(&self) -> Option<VerbatimParsedUrl> {
584        match self {
585            Self::Registry { .. } => None,
586            Self::Url {
587                location,
588                subdirectory,
589                ext,
590                url,
591            } => Some(VerbatimParsedUrl {
592                parsed_url: ParsedUrl::Archive(ParsedArchiveUrl::from_source(
593                    location.clone(),
594                    subdirectory.clone(),
595                    *ext,
596                )),
597                verbatim: url.clone(),
598            }),
599            Self::Path {
600                install_path,
601                ext,
602                url,
603            } => Some(VerbatimParsedUrl {
604                parsed_url: ParsedUrl::Path(ParsedPathUrl::from_source(
605                    install_path.clone(),
606                    *ext,
607                    url.to_url(),
608                )),
609                verbatim: url.clone(),
610            }),
611            Self::Directory {
612                install_path,
613                editable,
614                r#virtual,
615                url,
616            } => Some(VerbatimParsedUrl {
617                parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl::from_source(
618                    install_path.clone(),
619                    *editable,
620                    *r#virtual,
621                    url.to_url(),
622                )),
623                verbatim: url.clone(),
624            }),
625            Self::Git {
626                git,
627                subdirectory,
628                url,
629            } => Some(VerbatimParsedUrl {
630                parsed_url: ParsedUrl::Git(ParsedGitUrl::from_source(
631                    git.clone(),
632                    subdirectory.clone(),
633                )),
634                verbatim: url.clone(),
635            }),
636        }
637    }
638
639    /// Convert the source to a version specifier or URL.
640    ///
641    /// If the source is a registry and the specifier is empty, it returns `None`.
642    pub fn version_or_url(&self) -> Option<VersionOrUrl<VerbatimParsedUrl>> {
643        match self {
644            Self::Registry { specifier, .. } => {
645                if specifier.is_empty() {
646                    None
647                } else {
648                    Some(VersionOrUrl::VersionSpecifier(specifier.clone()))
649                }
650            }
651            Self::Url { .. } | Self::Git { .. } | Self::Path { .. } | Self::Directory { .. } => {
652                Some(VersionOrUrl::Url(self.to_verbatim_parsed_url()?))
653            }
654        }
655    }
656
657    /// Returns `true` if the source is editable.
658    pub fn is_editable(&self) -> bool {
659        matches!(
660            self,
661            Self::Directory {
662                editable: Some(true),
663                ..
664            }
665        )
666    }
667
668    /// Returns `true` if the source is empty.
669    pub fn is_empty(&self) -> bool {
670        match self {
671            Self::Registry { specifier, .. } => specifier.is_empty(),
672            Self::Url { .. } | Self::Git { .. } | Self::Path { .. } | Self::Directory { .. } => {
673                false
674            }
675        }
676    }
677
678    /// If the source is the registry, return the version specifiers
679    pub fn version_specifiers(&self) -> Option<&VersionSpecifiers> {
680        match self {
681            Self::Registry { specifier, .. } => Some(specifier),
682            Self::Url { .. } | Self::Git { .. } | Self::Path { .. } | Self::Directory { .. } => {
683                None
684            }
685        }
686    }
687
688    /// Convert the source to a [`RequirementSource`] relative to the given path.
689    pub fn relative_to(self, path: &Path) -> Result<Self, io::Error> {
690        match self {
691            Self::Registry { .. } | Self::Url { .. } | Self::Git { .. } => Ok(self),
692            Self::Path {
693                install_path,
694                ext,
695                url,
696            } => Ok(Self::Path {
697                install_path: relative_to(&install_path, path)
698                    .or_else(|_| std::path::absolute(install_path))?
699                    .into_boxed_path(),
700                ext,
701                url,
702            }),
703            Self::Directory {
704                install_path,
705                editable,
706                r#virtual,
707                url,
708                ..
709            } => Ok(Self::Directory {
710                install_path: relative_to(&install_path, path)
711                    .or_else(|_| std::path::absolute(install_path))?
712                    .into_boxed_path(),
713                editable,
714                r#virtual,
715                url,
716            }),
717        }
718    }
719
720    /// Convert the source to a [`RequirementSource`] with an absolute path based on the given root.
721    #[must_use]
722    pub fn to_absolute(self, root: &Path) -> Self {
723        match self {
724            Self::Registry { .. } | Self::Url { .. } | Self::Git { .. } => self,
725            Self::Path {
726                install_path,
727                ext,
728                url,
729            } => Self::Path {
730                install_path: uv_fs::normalize_path_buf(root.join(install_path)).into_boxed_path(),
731                ext,
732                url,
733            },
734            Self::Directory {
735                install_path,
736                editable,
737                r#virtual,
738                url,
739                ..
740            } => Self::Directory {
741                install_path: uv_fs::normalize_path_buf(root.join(install_path)).into_boxed_path(),
742                editable,
743                r#virtual,
744                url,
745            },
746        }
747    }
748}
749
750impl Display for RequirementSource {
751    /// Display the [`RequirementSource`], with the intention of being shown directly to a user,
752    /// rather than for inclusion in a `requirements.txt` file.
753    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
754        match self {
755            Self::Registry {
756                specifier, index, ..
757            } => {
758                write!(f, "{specifier}")?;
759                if let Some(index) = index {
760                    write!(f, " (index: {})", index.url)?;
761                }
762            }
763            Self::Url { url, .. } => {
764                write!(f, " {url}")?;
765            }
766            Self::Git {
767                url: _,
768                git,
769                subdirectory,
770            } => {
771                write!(f, " git+{}", git.repository())?;
772                if let Some(reference) = git.reference().as_str() {
773                    write!(f, "@{reference}")?;
774                }
775                if let Some(subdirectory) = subdirectory {
776                    writeln!(f, "#subdirectory={}", subdirectory.display())?;
777                }
778                if git.lfs().enabled() {
779                    writeln!(
780                        f,
781                        "{}lfs=true",
782                        if subdirectory.is_some() { "&" } else { "#" }
783                    )?;
784                }
785            }
786            Self::Path { url, .. } => {
787                write!(f, "{url}")?;
788            }
789            Self::Directory { url, .. } => {
790                write!(f, "{url}")?;
791            }
792        }
793        Ok(())
794    }
795}
796
797#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
798#[serde(untagged)]
799enum RequirementSourceWire {
800    /// Ex) `source = { git = "<https://github.com/astral-test/uv-public-pypackage?rev=0.0.1#0dacfd662c64cb4ceb16e6cf65a157a8b715b979>" }`
801    Git { git: String },
802    /// Ex) `source = { url = "<https://example.org/foo-1.0.zip>" }`
803    Direct {
804        url: DisplaySafeUrl,
805        subdirectory: Option<PortablePathBuf>,
806    },
807    /// Ex) `source = { path = "/home/ferris/iniconfig-2.0.0-py3-none-any.whl" }`
808    Path { path: PortablePathBuf },
809    /// Ex) `source = { directory = "/home/ferris/iniconfig" }`
810    Directory { directory: PortablePathBuf },
811    /// Ex) `source = { editable = "/home/ferris/iniconfig" }`
812    Editable { editable: PortablePathBuf },
813    /// Ex) `source = { editable = "/home/ferris/iniconfig" }`
814    Virtual { r#virtual: PortablePathBuf },
815    /// Ex) `source = { specifier = "foo >1,<2" }`
816    Registry {
817        #[serde(skip_serializing_if = "VersionSpecifiers::is_empty", default)]
818        specifier: VersionSpecifiers,
819        index: Option<DisplaySafeUrl>,
820        conflict: Option<ConflictItem>,
821    },
822}
823
824impl From<RequirementSource> for RequirementSourceWire {
825    fn from(value: RequirementSource) -> Self {
826        match value {
827            RequirementSource::Registry {
828                specifier,
829                index,
830                conflict,
831            } => {
832                let index = index.map(|index| index.url.into_url()).map(|mut index| {
833                    index.remove_credentials();
834                    index
835                });
836                Self::Registry {
837                    specifier,
838                    index,
839                    conflict,
840                }
841            }
842            RequirementSource::Url {
843                subdirectory,
844                location,
845                ext: _,
846                url: _,
847            } => Self::Direct {
848                url: location,
849                subdirectory: subdirectory.map(PortablePathBuf::from),
850            },
851            RequirementSource::Git {
852                git,
853                subdirectory,
854                url: _,
855            } => {
856                let mut url = git.repository().clone();
857
858                // Remove the credentials.
859                url.remove_credentials();
860
861                // Clear out any existing state.
862                url.set_fragment(None);
863                url.set_query(None);
864
865                // Put the subdirectory in the query.
866                if let Some(subdirectory) = subdirectory
867                    .as_deref()
868                    .map(PortablePath::from)
869                    .as_ref()
870                    .map(PortablePath::to_string)
871                {
872                    url.query_pairs_mut()
873                        .append_pair("subdirectory", &subdirectory);
874                }
875
876                // Persist lfs=true in the distribution metadata only when explicitly enabled.
877                if git.lfs().enabled() {
878                    url.query_pairs_mut().append_pair("lfs", "true");
879                }
880
881                // Put the requested reference in the query.
882                match git.reference() {
883                    GitReference::Branch(branch) => {
884                        url.query_pairs_mut().append_pair("branch", branch.as_str());
885                    }
886                    GitReference::Tag(tag) => {
887                        url.query_pairs_mut().append_pair("tag", tag.as_str());
888                    }
889                    GitReference::BranchOrTag(rev)
890                    | GitReference::BranchOrTagOrCommit(rev)
891                    | GitReference::NamedRef(rev) => {
892                        url.query_pairs_mut().append_pair("rev", rev.as_str());
893                    }
894                    GitReference::DefaultBranch => {}
895                }
896
897                // Put the precise commit in the fragment.
898                if let Some(precise) = git.precise() {
899                    url.set_fragment(Some(&precise.to_string()));
900                }
901
902                Self::Git {
903                    git: url.to_string(),
904                }
905            }
906            RequirementSource::Path {
907                install_path,
908                ext: _,
909                url: _,
910            } => Self::Path {
911                path: PortablePathBuf::from(install_path),
912            },
913            RequirementSource::Directory {
914                install_path,
915                editable,
916                r#virtual,
917                url: _,
918            } => {
919                if editable.unwrap_or(false) {
920                    Self::Editable {
921                        editable: PortablePathBuf::from(install_path),
922                    }
923                } else if r#virtual.unwrap_or(false) {
924                    Self::Virtual {
925                        r#virtual: PortablePathBuf::from(install_path),
926                    }
927                } else {
928                    Self::Directory {
929                        directory: PortablePathBuf::from(install_path),
930                    }
931                }
932            }
933        }
934    }
935}
936
937impl TryFrom<RequirementSourceWire> for RequirementSource {
938    type Error = RequirementError;
939
940    fn try_from(wire: RequirementSourceWire) -> Result<Self, RequirementError> {
941        match wire {
942            RequirementSourceWire::Registry {
943                specifier,
944                index,
945                conflict,
946            } => Ok(Self::Registry {
947                specifier,
948                index: index
949                    .map(|index| IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url(index)))),
950                conflict,
951            }),
952            RequirementSourceWire::Git { git } => {
953                let mut repository = DisplaySafeUrl::parse(&git)?;
954
955                let mut reference = GitReference::DefaultBranch;
956                let mut subdirectory: Option<PortablePathBuf> = None;
957                let mut lfs = GitLfs::Disabled;
958                for (key, val) in repository.query_pairs() {
959                    match &*key {
960                        "tag" => reference = GitReference::Tag(val.into_owned()),
961                        "branch" => reference = GitReference::Branch(val.into_owned()),
962                        "rev" => reference = GitReference::from_rev(val.into_owned()),
963                        "subdirectory" => {
964                            subdirectory = Some(PortablePathBuf::from(val.as_ref()));
965                        }
966                        "lfs" => lfs = GitLfs::from(val.eq_ignore_ascii_case("true")),
967                        _ => {}
968                    }
969                }
970
971                let precise = repository.fragment().map(GitOid::from_str).transpose()?;
972
973                // Clear out any existing state.
974                repository.set_fragment(None);
975                repository.set_query(None);
976
977                // Remove the credentials.
978                repository.remove_credentials();
979
980                // Create a PEP 508-compatible URL.
981                let mut url = DisplaySafeUrl::parse(&format!("git+{repository}"))?;
982                if let Some(rev) = reference.as_str() {
983                    let path = format!("{}@{}", url.path(), rev);
984                    url.set_path(&path);
985                }
986                let mut frags: Vec<String> = Vec::new();
987                if let Some(subdirectory) = subdirectory.as_ref() {
988                    frags.push(format!("subdirectory={subdirectory}"));
989                }
990                // Preserve that we're using Git LFS in the Verbatim Url representations
991                if lfs.enabled() {
992                    frags.push("lfs=true".to_string());
993                }
994                if !frags.is_empty() {
995                    url.set_fragment(Some(&frags.join("&")));
996                }
997
998                let url = VerbatimUrl::from_url(url);
999
1000                Ok(Self::Git {
1001                    git: GitUrl::from_fields(repository, reference, precise, lfs)?,
1002                    subdirectory: subdirectory.map(Box::<Path>::from),
1003                    url,
1004                })
1005            }
1006            RequirementSourceWire::Direct { url, subdirectory } => {
1007                let location = url.clone();
1008
1009                // Create a PEP 508-compatible URL.
1010                let mut url = url.clone();
1011                if let Some(subdirectory) = &subdirectory {
1012                    url.set_fragment(Some(&format!("subdirectory={subdirectory}")));
1013                }
1014
1015                Ok(Self::Url {
1016                    location,
1017                    subdirectory: subdirectory.map(Box::<Path>::from),
1018                    ext: DistExtension::from_path(url.path())
1019                        .map_err(|err| ParsedUrlError::MissingExtensionUrl(url.to_string(), err))?,
1020                    url: VerbatimUrl::from_url(url.clone()),
1021                })
1022            }
1023            // TODO(charlie): The use of `CWD` here is incorrect. These should be resolved relative
1024            // to the workspace root, but we don't have access to it here. When comparing these
1025            // sources in the lockfile, we replace the URL anyway. Ideally, we'd either remove the
1026            // URL field or make it optional.
1027            RequirementSourceWire::Path { path } => {
1028                let path = Box::<Path>::from(path);
1029                let url =
1030                    VerbatimUrl::from_normalized_path(uv_fs::normalize_path_buf(CWD.join(&path)))?;
1031                Ok(Self::Path {
1032                    ext: DistExtension::from_path(&path).map_err(|err| {
1033                        ParsedUrlError::MissingExtensionPath(path.to_path_buf(), err)
1034                    })?,
1035                    install_path: path,
1036                    url,
1037                })
1038            }
1039            RequirementSourceWire::Directory { directory } => {
1040                let directory = Box::<Path>::from(directory);
1041                let url = VerbatimUrl::from_normalized_path(uv_fs::normalize_path_buf(
1042                    CWD.join(&directory),
1043                ))?;
1044                Ok(Self::Directory {
1045                    install_path: directory,
1046                    editable: Some(false),
1047                    r#virtual: Some(false),
1048                    url,
1049                })
1050            }
1051            RequirementSourceWire::Editable { editable } => {
1052                let editable = Box::<Path>::from(editable);
1053                let url = VerbatimUrl::from_normalized_path(uv_fs::normalize_path_buf(
1054                    CWD.join(&editable),
1055                ))?;
1056                Ok(Self::Directory {
1057                    install_path: editable,
1058                    editable: Some(true),
1059                    r#virtual: Some(false),
1060                    url,
1061                })
1062            }
1063            RequirementSourceWire::Virtual { r#virtual } => {
1064                let r#virtual = Box::<Path>::from(r#virtual);
1065                let url = VerbatimUrl::from_normalized_path(uv_fs::normalize_path_buf(
1066                    CWD.join(&r#virtual),
1067                ))?;
1068                Ok(Self::Directory {
1069                    install_path: r#virtual,
1070                    editable: Some(false),
1071                    r#virtual: Some(true),
1072                    url,
1073                })
1074            }
1075        }
1076    }
1077}
1078
1079#[cfg(test)]
1080mod tests {
1081    use std::path::PathBuf;
1082
1083    use uv_pep508::{MarkerTree, VerbatimUrl};
1084
1085    use crate::{Requirement, RequirementSource};
1086
1087    #[test]
1088    fn roundtrip() {
1089        let requirement = Requirement {
1090            name: "foo".parse().unwrap(),
1091            extras: Box::new([]),
1092            groups: Box::new([]),
1093            marker: MarkerTree::TRUE,
1094            source: RequirementSource::Registry {
1095                specifier: ">1,<2".parse().unwrap(),
1096                index: None,
1097                conflict: None,
1098            },
1099            origin: None,
1100        };
1101
1102        let raw = toml::to_string(&requirement).unwrap();
1103        let deserialized: Requirement = toml::from_str(&raw).unwrap();
1104        assert_eq!(requirement, deserialized);
1105
1106        let path = if cfg!(windows) {
1107            "C:\\home\\ferris\\foo"
1108        } else {
1109            "/home/ferris/foo"
1110        };
1111        let requirement = Requirement {
1112            name: "foo".parse().unwrap(),
1113            extras: Box::new([]),
1114            groups: Box::new([]),
1115            marker: MarkerTree::TRUE,
1116            source: RequirementSource::Directory {
1117                install_path: PathBuf::from(path).into_boxed_path(),
1118                editable: Some(false),
1119                r#virtual: Some(false),
1120                url: VerbatimUrl::from_absolute_path(path).unwrap(),
1121            },
1122            origin: None,
1123        };
1124
1125        let raw = toml::to_string(&requirement).unwrap();
1126        let deserialized: Requirement = toml::from_str(&raw).unwrap();
1127        assert_eq!(requirement, deserialized);
1128    }
1129}