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::{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            }
354            RequirementSource::Path { url, .. } => {
355                write!(f, " @ {url}")?;
356            }
357            RequirementSource::Directory { url, .. } => {
358                write!(f, " @ {url}")?;
359            }
360        }
361        if let Some(marker) = self.marker.contents() {
362            write!(f, " ; {marker}")?;
363        }
364        Ok(())
365    }
366}
367
368impl CacheKey for Requirement {
369    fn cache_key(&self, state: &mut CacheKeyHasher) {
370        self.name.as_str().cache_key(state);
371
372        self.groups.len().cache_key(state);
373        for group in &self.groups {
374            group.as_str().cache_key(state);
375        }
376
377        self.extras.len().cache_key(state);
378        for extra in &self.extras {
379            extra.as_str().cache_key(state);
380        }
381
382        if let Some(marker) = self.marker.contents() {
383            1u8.cache_key(state);
384            marker.to_string().cache_key(state);
385        } else {
386            0u8.cache_key(state);
387        }
388
389        match &self.source {
390            RequirementSource::Registry {
391                specifier,
392                index,
393                conflict: _,
394            } => {
395                0u8.cache_key(state);
396                specifier.len().cache_key(state);
397                for spec in specifier.iter() {
398                    spec.operator().as_str().cache_key(state);
399                    spec.version().cache_key(state);
400                }
401                if let Some(index) = index {
402                    1u8.cache_key(state);
403                    index.url.cache_key(state);
404                } else {
405                    0u8.cache_key(state);
406                }
407                // `conflict` is intentionally omitted
408            }
409            RequirementSource::Url {
410                location,
411                subdirectory,
412                ext,
413                url,
414            } => {
415                1u8.cache_key(state);
416                location.cache_key(state);
417                if let Some(subdirectory) = subdirectory {
418                    1u8.cache_key(state);
419                    subdirectory.display().to_string().cache_key(state);
420                } else {
421                    0u8.cache_key(state);
422                }
423                ext.name().cache_key(state);
424                url.cache_key(state);
425            }
426            RequirementSource::Git {
427                git,
428                subdirectory,
429                url,
430            } => {
431                2u8.cache_key(state);
432                git.to_string().cache_key(state);
433                if let Some(subdirectory) = subdirectory {
434                    1u8.cache_key(state);
435                    subdirectory.display().to_string().cache_key(state);
436                } else {
437                    0u8.cache_key(state);
438                }
439                url.cache_key(state);
440            }
441            RequirementSource::Path {
442                install_path,
443                ext,
444                url,
445            } => {
446                3u8.cache_key(state);
447                install_path.cache_key(state);
448                ext.name().cache_key(state);
449                url.cache_key(state);
450            }
451            RequirementSource::Directory {
452                install_path,
453                editable,
454                r#virtual,
455                url,
456            } => {
457                4u8.cache_key(state);
458                install_path.cache_key(state);
459                editable.cache_key(state);
460                r#virtual.cache_key(state);
461                url.cache_key(state);
462            }
463        }
464
465        // `origin` is intentionally omitted
466    }
467}
468
469/// The different locations with can install a distribution from: Version specifier (from an index),
470/// HTTP(S) URL, git repository, and path.
471///
472/// We store both the parsed fields (such as the plain url and the subdirectory) and the joined
473/// PEP 508 style url (e.g. `file:///<path>#subdirectory=<subdirectory>`) since we need both in
474/// different locations.
475#[derive(
476    Hash, Debug, Clone, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, serde::Deserialize,
477)]
478#[serde(try_from = "RequirementSourceWire", into = "RequirementSourceWire")]
479pub enum RequirementSource {
480    /// The requirement has a version specifier, such as `foo >1,<2`.
481    Registry {
482        specifier: VersionSpecifiers,
483        /// Choose a version from the index at the given URL.
484        index: Option<IndexMetadata>,
485        /// The conflict item associated with the source, if any.
486        conflict: Option<ConflictItem>,
487    },
488    // TODO(konsti): Track and verify version specifier from `project.dependencies` matches the
489    // version in remote location.
490    /// A remote `http://` or `https://` URL, either a built distribution,
491    /// e.g. `foo @ https://example.org/foo-1.0-py3-none-any.whl`, or a source distribution,
492    /// e.g.`foo @ https://example.org/foo-1.0.zip`.
493    Url {
494        /// The remote location of the archive file, without subdirectory fragment.
495        location: DisplaySafeUrl,
496        /// For source distributions, the path to the distribution if it is not in the archive
497        /// root.
498        subdirectory: Option<Box<Path>>,
499        /// The file extension, e.g. `tar.gz`, `zip`, etc.
500        ext: DistExtension,
501        /// The PEP 508 style URL in the format
502        /// `<scheme>://<domain>/<path>#subdirectory=<subdirectory>`.
503        url: VerbatimUrl,
504    },
505    /// A remote Git repository, over either HTTPS or SSH.
506    Git {
507        /// The repository URL and reference to the commit to use.
508        git: GitUrl,
509        /// The path to the source distribution if it is not in the repository root.
510        subdirectory: Option<Box<Path>>,
511        /// The PEP 508 style url in the format
512        /// `git+<scheme>://<domain>/<path>@<rev>#subdirectory=<subdirectory>`.
513        url: VerbatimUrl,
514    },
515    /// A local built or source distribution, either from a path or a `file://` URL. It can either
516    /// be a binary distribution (a `.whl` file) or a source distribution archive (a `.zip` or
517    /// `.tar.gz` file).
518    Path {
519        /// The absolute path to the distribution which we use for installing.
520        install_path: Box<Path>,
521        /// The file extension, e.g. `tar.gz`, `zip`, etc.
522        ext: DistExtension,
523        /// The PEP 508 style URL in the format
524        /// `file:///<path>#subdirectory=<subdirectory>`.
525        url: VerbatimUrl,
526    },
527    /// A local source tree (a directory with a pyproject.toml in, or a legacy
528    /// source distribution with only a setup.py but non pyproject.toml in it).
529    Directory {
530        /// The absolute path to the distribution which we use for installing.
531        install_path: Box<Path>,
532        /// For a source tree (a directory), whether to install as an editable.
533        editable: Option<bool>,
534        /// For a source tree (a directory), whether the project should be built and installed.
535        r#virtual: Option<bool>,
536        /// The PEP 508 style URL in the format
537        /// `file:///<path>#subdirectory=<subdirectory>`.
538        url: VerbatimUrl,
539    },
540}
541
542impl RequirementSource {
543    /// Construct a [`RequirementSource`] for a URL source, given a URL parsed into components and
544    /// the PEP 508 string (after the `@`) as [`VerbatimUrl`].
545    pub fn from_parsed_url(parsed_url: ParsedUrl, url: VerbatimUrl) -> Self {
546        match parsed_url {
547            ParsedUrl::Path(local_file) => Self::Path {
548                install_path: local_file.install_path.clone(),
549                ext: local_file.ext,
550                url,
551            },
552            ParsedUrl::Directory(directory) => Self::Directory {
553                install_path: directory.install_path.clone(),
554                editable: directory.editable,
555                r#virtual: directory.r#virtual,
556                url,
557            },
558            ParsedUrl::Git(git) => Self::Git {
559                git: git.url.clone(),
560                url,
561                subdirectory: git.subdirectory,
562            },
563            ParsedUrl::Archive(archive) => Self::Url {
564                url,
565                location: archive.url,
566                subdirectory: archive.subdirectory,
567                ext: archive.ext,
568            },
569        }
570    }
571
572    /// Convert the source to a [`VerbatimParsedUrl`], if it's a URL source.
573    pub fn to_verbatim_parsed_url(&self) -> Option<VerbatimParsedUrl> {
574        match self {
575            Self::Registry { .. } => None,
576            Self::Url {
577                location,
578                subdirectory,
579                ext,
580                url,
581            } => Some(VerbatimParsedUrl {
582                parsed_url: ParsedUrl::Archive(ParsedArchiveUrl::from_source(
583                    location.clone(),
584                    subdirectory.clone(),
585                    *ext,
586                )),
587                verbatim: url.clone(),
588            }),
589            Self::Path {
590                install_path,
591                ext,
592                url,
593            } => Some(VerbatimParsedUrl {
594                parsed_url: ParsedUrl::Path(ParsedPathUrl::from_source(
595                    install_path.clone(),
596                    *ext,
597                    url.to_url(),
598                )),
599                verbatim: url.clone(),
600            }),
601            Self::Directory {
602                install_path,
603                editable,
604                r#virtual,
605                url,
606            } => Some(VerbatimParsedUrl {
607                parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl::from_source(
608                    install_path.clone(),
609                    *editable,
610                    *r#virtual,
611                    url.to_url(),
612                )),
613                verbatim: url.clone(),
614            }),
615            Self::Git {
616                git,
617                subdirectory,
618                url,
619            } => Some(VerbatimParsedUrl {
620                parsed_url: ParsedUrl::Git(ParsedGitUrl::from_source(
621                    git.clone(),
622                    subdirectory.clone(),
623                )),
624                verbatim: url.clone(),
625            }),
626        }
627    }
628
629    /// Convert the source to a version specifier or URL.
630    ///
631    /// If the source is a registry and the specifier is empty, it returns `None`.
632    pub fn version_or_url(&self) -> Option<VersionOrUrl<VerbatimParsedUrl>> {
633        match self {
634            Self::Registry { specifier, .. } => {
635                if specifier.is_empty() {
636                    None
637                } else {
638                    Some(VersionOrUrl::VersionSpecifier(specifier.clone()))
639                }
640            }
641            Self::Url { .. } | Self::Git { .. } | Self::Path { .. } | Self::Directory { .. } => {
642                Some(VersionOrUrl::Url(self.to_verbatim_parsed_url()?))
643            }
644        }
645    }
646
647    /// Returns `true` if the source is editable.
648    pub fn is_editable(&self) -> bool {
649        matches!(
650            self,
651            Self::Directory {
652                editable: Some(true),
653                ..
654            }
655        )
656    }
657
658    /// Returns `true` if the source is empty.
659    pub fn is_empty(&self) -> bool {
660        match self {
661            Self::Registry { specifier, .. } => specifier.is_empty(),
662            Self::Url { .. } | Self::Git { .. } | Self::Path { .. } | Self::Directory { .. } => {
663                false
664            }
665        }
666    }
667
668    /// If the source is the registry, return the version specifiers
669    pub fn version_specifiers(&self) -> Option<&VersionSpecifiers> {
670        match self {
671            Self::Registry { specifier, .. } => Some(specifier),
672            Self::Url { .. } | Self::Git { .. } | Self::Path { .. } | Self::Directory { .. } => {
673                None
674            }
675        }
676    }
677
678    /// Convert the source to a [`RequirementSource`] relative to the given path.
679    pub fn relative_to(self, path: &Path) -> Result<Self, io::Error> {
680        match self {
681            Self::Registry { .. } | Self::Url { .. } | Self::Git { .. } => Ok(self),
682            Self::Path {
683                install_path,
684                ext,
685                url,
686            } => Ok(Self::Path {
687                install_path: relative_to(&install_path, path)
688                    .or_else(|_| std::path::absolute(install_path))?
689                    .into_boxed_path(),
690                ext,
691                url,
692            }),
693            Self::Directory {
694                install_path,
695                editable,
696                r#virtual,
697                url,
698                ..
699            } => Ok(Self::Directory {
700                install_path: relative_to(&install_path, path)
701                    .or_else(|_| std::path::absolute(install_path))?
702                    .into_boxed_path(),
703                editable,
704                r#virtual,
705                url,
706            }),
707        }
708    }
709
710    /// Convert the source to a [`RequirementSource`] with an absolute path based on the given root.
711    #[must_use]
712    pub fn to_absolute(self, root: &Path) -> Self {
713        match self {
714            Self::Registry { .. } | Self::Url { .. } | Self::Git { .. } => self,
715            Self::Path {
716                install_path,
717                ext,
718                url,
719            } => Self::Path {
720                install_path: uv_fs::normalize_path_buf(root.join(install_path)).into_boxed_path(),
721                ext,
722                url,
723            },
724            Self::Directory {
725                install_path,
726                editable,
727                r#virtual,
728                url,
729                ..
730            } => Self::Directory {
731                install_path: uv_fs::normalize_path_buf(root.join(install_path)).into_boxed_path(),
732                editable,
733                r#virtual,
734                url,
735            },
736        }
737    }
738}
739
740impl Display for RequirementSource {
741    /// Display the [`RequirementSource`], with the intention of being shown directly to a user,
742    /// rather than for inclusion in a `requirements.txt` file.
743    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
744        match self {
745            Self::Registry {
746                specifier, index, ..
747            } => {
748                write!(f, "{specifier}")?;
749                if let Some(index) = index {
750                    write!(f, " (index: {})", index.url)?;
751                }
752            }
753            Self::Url { url, .. } => {
754                write!(f, " {url}")?;
755            }
756            Self::Git {
757                url: _,
758                git,
759                subdirectory,
760            } => {
761                write!(f, " git+{}", git.repository())?;
762                if let Some(reference) = git.reference().as_str() {
763                    write!(f, "@{reference}")?;
764                }
765                if let Some(subdirectory) = subdirectory {
766                    writeln!(f, "#subdirectory={}", subdirectory.display())?;
767                }
768            }
769            Self::Path { url, .. } => {
770                write!(f, "{url}")?;
771            }
772            Self::Directory { url, .. } => {
773                write!(f, "{url}")?;
774            }
775        }
776        Ok(())
777    }
778}
779
780#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
781#[serde(untagged)]
782enum RequirementSourceWire {
783    /// Ex) `source = { git = "<https://github.com/astral-test/uv-public-pypackage?rev=0.0.1#0dacfd662c64cb4ceb16e6cf65a157a8b715b979>" }`
784    Git { git: String },
785    /// Ex) `source = { url = "<https://example.org/foo-1.0.zip>" }`
786    Direct {
787        url: DisplaySafeUrl,
788        subdirectory: Option<PortablePathBuf>,
789    },
790    /// Ex) `source = { path = "/home/ferris/iniconfig-2.0.0-py3-none-any.whl" }`
791    Path { path: PortablePathBuf },
792    /// Ex) `source = { directory = "/home/ferris/iniconfig" }`
793    Directory { directory: PortablePathBuf },
794    /// Ex) `source = { editable = "/home/ferris/iniconfig" }`
795    Editable { editable: PortablePathBuf },
796    /// Ex) `source = { editable = "/home/ferris/iniconfig" }`
797    Virtual { r#virtual: PortablePathBuf },
798    /// Ex) `source = { specifier = "foo >1,<2" }`
799    Registry {
800        #[serde(skip_serializing_if = "VersionSpecifiers::is_empty", default)]
801        specifier: VersionSpecifiers,
802        index: Option<DisplaySafeUrl>,
803        conflict: Option<ConflictItem>,
804    },
805}
806
807impl From<RequirementSource> for RequirementSourceWire {
808    fn from(value: RequirementSource) -> Self {
809        match value {
810            RequirementSource::Registry {
811                specifier,
812                index,
813                conflict,
814            } => {
815                let index = index.map(|index| index.url.into_url()).map(|mut index| {
816                    index.remove_credentials();
817                    index
818                });
819                Self::Registry {
820                    specifier,
821                    index,
822                    conflict,
823                }
824            }
825            RequirementSource::Url {
826                subdirectory,
827                location,
828                ext: _,
829                url: _,
830            } => Self::Direct {
831                url: location,
832                subdirectory: subdirectory.map(PortablePathBuf::from),
833            },
834            RequirementSource::Git {
835                git,
836                subdirectory,
837                url: _,
838            } => {
839                let mut url = git.repository().clone();
840
841                // Remove the credentials.
842                url.remove_credentials();
843
844                // Clear out any existing state.
845                url.set_fragment(None);
846                url.set_query(None);
847
848                // Put the subdirectory in the query.
849                if let Some(subdirectory) = subdirectory
850                    .as_deref()
851                    .map(PortablePath::from)
852                    .as_ref()
853                    .map(PortablePath::to_string)
854                {
855                    url.query_pairs_mut()
856                        .append_pair("subdirectory", &subdirectory);
857                }
858
859                // Put the requested reference in the query.
860                match git.reference() {
861                    GitReference::Branch(branch) => {
862                        url.query_pairs_mut().append_pair("branch", branch.as_str());
863                    }
864                    GitReference::Tag(tag) => {
865                        url.query_pairs_mut().append_pair("tag", tag.as_str());
866                    }
867                    GitReference::BranchOrTag(rev)
868                    | GitReference::BranchOrTagOrCommit(rev)
869                    | GitReference::NamedRef(rev) => {
870                        url.query_pairs_mut().append_pair("rev", rev.as_str());
871                    }
872                    GitReference::DefaultBranch => {}
873                }
874
875                // Put the precise commit in the fragment.
876                if let Some(precise) = git.precise() {
877                    url.set_fragment(Some(&precise.to_string()));
878                }
879
880                Self::Git {
881                    git: url.to_string(),
882                }
883            }
884            RequirementSource::Path {
885                install_path,
886                ext: _,
887                url: _,
888            } => Self::Path {
889                path: PortablePathBuf::from(install_path),
890            },
891            RequirementSource::Directory {
892                install_path,
893                editable,
894                r#virtual,
895                url: _,
896            } => {
897                if editable.unwrap_or(false) {
898                    Self::Editable {
899                        editable: PortablePathBuf::from(install_path),
900                    }
901                } else if r#virtual.unwrap_or(false) {
902                    Self::Virtual {
903                        r#virtual: PortablePathBuf::from(install_path),
904                    }
905                } else {
906                    Self::Directory {
907                        directory: PortablePathBuf::from(install_path),
908                    }
909                }
910            }
911        }
912    }
913}
914
915impl TryFrom<RequirementSourceWire> for RequirementSource {
916    type Error = RequirementError;
917
918    fn try_from(wire: RequirementSourceWire) -> Result<Self, RequirementError> {
919        match wire {
920            RequirementSourceWire::Registry {
921                specifier,
922                index,
923                conflict,
924            } => Ok(Self::Registry {
925                specifier,
926                index: index
927                    .map(|index| IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url(index)))),
928                conflict,
929            }),
930            RequirementSourceWire::Git { git } => {
931                let mut repository = DisplaySafeUrl::parse(&git)?;
932
933                let mut reference = GitReference::DefaultBranch;
934                let mut subdirectory: Option<PortablePathBuf> = None;
935                for (key, val) in repository.query_pairs() {
936                    match &*key {
937                        "tag" => reference = GitReference::Tag(val.into_owned()),
938                        "branch" => reference = GitReference::Branch(val.into_owned()),
939                        "rev" => reference = GitReference::from_rev(val.into_owned()),
940                        "subdirectory" => {
941                            subdirectory = Some(PortablePathBuf::from(val.as_ref()));
942                        }
943                        _ => {}
944                    }
945                }
946
947                let precise = repository.fragment().map(GitOid::from_str).transpose()?;
948
949                // Clear out any existing state.
950                repository.set_fragment(None);
951                repository.set_query(None);
952
953                // Remove the credentials.
954                repository.remove_credentials();
955
956                // Create a PEP 508-compatible URL.
957                let mut url = DisplaySafeUrl::parse(&format!("git+{repository}"))?;
958                if let Some(rev) = reference.as_str() {
959                    let path = format!("{}@{}", url.path(), rev);
960                    url.set_path(&path);
961                }
962                if let Some(subdirectory) = subdirectory.as_ref() {
963                    url.set_fragment(Some(&format!("subdirectory={subdirectory}")));
964                }
965                let url = VerbatimUrl::from_url(url);
966
967                Ok(Self::Git {
968                    git: GitUrl::from_fields(repository, reference, precise)?,
969                    subdirectory: subdirectory.map(Box::<Path>::from),
970                    url,
971                })
972            }
973            RequirementSourceWire::Direct { url, subdirectory } => {
974                let location = url.clone();
975
976                // Create a PEP 508-compatible URL.
977                let mut url = url.clone();
978                if let Some(subdirectory) = &subdirectory {
979                    url.set_fragment(Some(&format!("subdirectory={subdirectory}")));
980                }
981
982                Ok(Self::Url {
983                    location,
984                    subdirectory: subdirectory.map(Box::<Path>::from),
985                    ext: DistExtension::from_path(url.path())
986                        .map_err(|err| ParsedUrlError::MissingExtensionUrl(url.to_string(), err))?,
987                    url: VerbatimUrl::from_url(url.clone()),
988                })
989            }
990            // TODO(charlie): The use of `CWD` here is incorrect. These should be resolved relative
991            // to the workspace root, but we don't have access to it here. When comparing these
992            // sources in the lockfile, we replace the URL anyway. Ideally, we'd either remove the
993            // URL field or make it optional.
994            RequirementSourceWire::Path { path } => {
995                let path = Box::<Path>::from(path);
996                let url =
997                    VerbatimUrl::from_normalized_path(uv_fs::normalize_path_buf(CWD.join(&path)))?;
998                Ok(Self::Path {
999                    ext: DistExtension::from_path(&path).map_err(|err| {
1000                        ParsedUrlError::MissingExtensionPath(path.to_path_buf(), err)
1001                    })?,
1002                    install_path: path,
1003                    url,
1004                })
1005            }
1006            RequirementSourceWire::Directory { directory } => {
1007                let directory = Box::<Path>::from(directory);
1008                let url = VerbatimUrl::from_normalized_path(uv_fs::normalize_path_buf(
1009                    CWD.join(&directory),
1010                ))?;
1011                Ok(Self::Directory {
1012                    install_path: directory,
1013                    editable: Some(false),
1014                    r#virtual: Some(false),
1015                    url,
1016                })
1017            }
1018            RequirementSourceWire::Editable { editable } => {
1019                let editable = Box::<Path>::from(editable);
1020                let url = VerbatimUrl::from_normalized_path(uv_fs::normalize_path_buf(
1021                    CWD.join(&editable),
1022                ))?;
1023                Ok(Self::Directory {
1024                    install_path: editable,
1025                    editable: Some(true),
1026                    r#virtual: Some(false),
1027                    url,
1028                })
1029            }
1030            RequirementSourceWire::Virtual { r#virtual } => {
1031                let r#virtual = Box::<Path>::from(r#virtual);
1032                let url = VerbatimUrl::from_normalized_path(uv_fs::normalize_path_buf(
1033                    CWD.join(&r#virtual),
1034                ))?;
1035                Ok(Self::Directory {
1036                    install_path: r#virtual,
1037                    editable: Some(false),
1038                    r#virtual: Some(true),
1039                    url,
1040                })
1041            }
1042        }
1043    }
1044}
1045
1046#[cfg(test)]
1047mod tests {
1048    use std::path::PathBuf;
1049
1050    use uv_pep508::{MarkerTree, VerbatimUrl};
1051
1052    use crate::{Requirement, RequirementSource};
1053
1054    #[test]
1055    fn roundtrip() {
1056        let requirement = Requirement {
1057            name: "foo".parse().unwrap(),
1058            extras: Box::new([]),
1059            groups: Box::new([]),
1060            marker: MarkerTree::TRUE,
1061            source: RequirementSource::Registry {
1062                specifier: ">1,<2".parse().unwrap(),
1063                index: None,
1064                conflict: None,
1065            },
1066            origin: None,
1067        };
1068
1069        let raw = toml::to_string(&requirement).unwrap();
1070        let deserialized: Requirement = toml::from_str(&raw).unwrap();
1071        assert_eq!(requirement, deserialized);
1072
1073        let path = if cfg!(windows) {
1074            "C:\\home\\ferris\\foo"
1075        } else {
1076            "/home/ferris/foo"
1077        };
1078        let requirement = Requirement {
1079            name: "foo".parse().unwrap(),
1080            extras: Box::new([]),
1081            groups: Box::new([]),
1082            marker: MarkerTree::TRUE,
1083            source: RequirementSource::Directory {
1084                install_path: PathBuf::from(path).into_boxed_path(),
1085                editable: Some(false),
1086                r#virtual: Some(false),
1087                url: VerbatimUrl::from_absolute_path(path).unwrap(),
1088            },
1089            origin: None,
1090        };
1091
1092        let raw = toml::to_string(&requirement).unwrap();
1093        let deserialized: Requirement = toml::from_str(&raw).unwrap();
1094        assert_eq!(requirement, deserialized);
1095    }
1096}