Skip to main content

uv_distribution_types/
requirement.rs

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