Skip to main content

uv_distribution_types/
lib.rs

1//! ## Type hierarchy
2//!
3//! When we receive the requirements from `pip sync`, we check which requirements already fulfilled
4//! in the users environment ([`InstalledDist`]), whether the matching package is in our wheel cache
5//! ([`CachedDist`]) or whether we need to download, (potentially build) and install it ([`Dist`]).
6//!
7//! ## `Dist`
8//! A [`Dist`] is either a built distribution (a wheel), or a source distribution that exists at
9//! some location. We translate every PEP 508 requirement e.g. from `requirements.txt` or from
10//! `pyproject.toml`'s `[project] dependencies` into a [`Dist`] by checking each index.
11//! * [`BuiltDist`]: A wheel, with its four possible origins:
12//!   * [`RegistryBuiltDist`]
13//!   * [`DirectUrlBuiltDist`]
14//!   * [`PathBuiltDist`]
15//!   * [`GitPathBuiltDist`]
16//! * [`SourceDist`]: A source distribution, with its six possible origins:
17//!   * [`RegistrySourceDist`]
18//!   * [`DirectUrlSourceDist`]
19//!   * [`GitDirectorySourceDist`]
20//!   * [`GitPathSourceDist`]
21//!   * [`PathSourceDist`]
22//!   * [`DirectorySourceDist`]
23//!
24//! ## `CachedDist`
25//! A [`CachedDist`] is a built distribution (wheel) that exists in the local cache, with the two
26//! possible origins we currently track:
27//! * [`CachedRegistryDist`]
28//! * [`CachedDirectUrlDist`]
29//!
30//! ## `InstalledDist`
31//! An [`InstalledDist`] is a distribution installed in a Python environment, with the five kinds
32//! we currently track:
33//! * [`InstalledRegistryDist`]
34//! * [`InstalledDirectUrlDist`]
35//! * [`InstalledEggInfoFile`]
36//! * [`InstalledEggInfoDirectory`]
37//! * [`InstalledLegacyEditable`]
38//!
39//! Direct URL information for an [`InstalledDirectUrlDist`] comes from
40//! [`direct_url.json`](https://packaging.python.org/en/latest/specifications/direct-url-data-structure/)
41//! and may not match the original [`Dist`] exactly.
42use std::borrow::Cow;
43use std::ffi::OsStr;
44use std::fmt::Display;
45use std::path;
46use std::path::{Path, PathBuf};
47use std::str::FromStr;
48
49use url::Url;
50
51use uv_distribution_filename::{
52    DistExtension, SourceDistExtension, SourceDistFilename, WheelFilename,
53};
54use uv_fs::normalize_absolute_path;
55use uv_git_types::GitUrl;
56use uv_normalize::PackageName;
57use uv_pep440::Version;
58use uv_pep508::{Pep508Url, VerbatimUrl};
59use uv_pypi_types::{
60    ParsedArchiveUrl, ParsedDirectoryUrl, ParsedGitDirectoryUrl, ParsedGitPathUrl, ParsedPathUrl,
61    ParsedUrl, VerbatimParsedUrl,
62};
63use uv_redacted::DisplaySafeUrl;
64
65pub use crate::annotation::*;
66pub use crate::any::*;
67pub use crate::build_info::*;
68pub use crate::build_requires::*;
69pub use crate::buildable::*;
70pub use crate::cached::*;
71pub use crate::config_settings::*;
72pub use crate::dependency_metadata::*;
73pub use crate::diagnostic::*;
74pub use crate::dist_error::*;
75pub use crate::error::*;
76pub use crate::exclude_newer::*;
77pub use crate::file::*;
78pub use crate::hash::*;
79pub use crate::id::*;
80pub use crate::index::*;
81pub use crate::index_name::*;
82pub use crate::index_url::*;
83pub use crate::installed::*;
84pub use crate::known_platform::*;
85pub use crate::origin::*;
86pub use crate::pip_index::*;
87pub use crate::prioritized_distribution::*;
88pub use crate::requested::*;
89pub use crate::requirement::*;
90pub use crate::requires_python::*;
91pub use crate::resolution::*;
92pub use crate::resolved::*;
93pub use crate::specified_requirement::*;
94pub use crate::status_code_strategy::*;
95pub use crate::traits::*;
96
97mod annotation;
98mod any;
99mod build_info;
100mod build_requires;
101mod buildable;
102mod cached;
103mod config_settings;
104mod dependency_metadata;
105mod diagnostic;
106mod dist_error;
107mod error;
108mod exclude_newer;
109mod file;
110mod hash;
111mod id;
112mod index;
113mod index_name;
114mod index_url;
115mod installed;
116mod installed_modules;
117mod known_platform;
118mod origin;
119mod pip_index;
120mod prioritized_distribution;
121mod requested;
122mod requirement;
123mod requires_python;
124mod resolution;
125mod resolved;
126mod specified_requirement;
127mod status_code_strategy;
128mod traits;
129
130#[derive(Debug, Clone)]
131pub enum VersionOrUrlRef<'a, T: Pep508Url = VerbatimUrl> {
132    /// A PEP 440 version specifier, used to identify a distribution in a registry.
133    Version(&'a Version),
134    /// A URL, used to identify a distribution at an arbitrary location.
135    Url(&'a T),
136}
137
138impl Verbatim for VersionOrUrlRef<'_> {
139    fn verbatim(&self) -> Cow<'_, str> {
140        match self {
141            Self::Version(version) => Cow::Owned(format!("=={version}")),
142            Self::Url(url) => Cow::Owned(format!(" @ {}", url.verbatim())),
143        }
144    }
145}
146
147impl std::fmt::Display for VersionOrUrlRef<'_> {
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        match self {
150            Self::Version(version) => write!(f, "=={version}"),
151            Self::Url(url) => write!(f, " @ {url}"),
152        }
153    }
154}
155
156#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
157pub enum InstalledVersion<'a> {
158    /// A PEP 440 version specifier, used to identify a distribution in a registry.
159    Version(&'a Version),
160    /// A URL, used to identify a distribution at an arbitrary location, along with the version
161    /// specifier to which it resolved.
162    Url(&'a DisplaySafeUrl, &'a Version),
163}
164
165impl<'a> InstalledVersion<'a> {
166    /// If it is a version, return its value.
167    pub fn version(&self) -> &'a Version {
168        match self {
169            Self::Version(version) => version,
170            Self::Url(_, version) => version,
171        }
172    }
173}
174
175impl std::fmt::Display for InstalledVersion<'_> {
176    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177        match self {
178            Self::Version(version) => write!(f, "=={version}"),
179            Self::Url(url, version) => write!(f, "=={version} (from {url})"),
180        }
181    }
182}
183
184/// Either a built distribution (a wheel) or a source distribution that exists at some location.
185///
186/// The location can be an index, URL, path, or Git repository (wheel or source distribution).
187#[derive(Debug, Clone, Hash, PartialEq, Eq)]
188pub enum Dist {
189    Built(BuiltDist),
190    Source(SourceDist),
191}
192
193/// A reference to a built or source distribution.
194#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
195pub enum DistRef<'a> {
196    Built(&'a BuiltDist),
197    Source(&'a SourceDist),
198}
199
200impl Display for DistRef<'_> {
201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202        match self {
203            Self::Built(built_dist) => Display::fmt(&built_dist, f),
204            Self::Source(source_dist) => Display::fmt(&source_dist, f),
205        }
206    }
207}
208
209/// A wheel, with its four possible origins (index, URL, path, or Git path)
210#[derive(Debug, Clone, Hash, PartialEq, Eq)]
211pub enum BuiltDist {
212    Registry(RegistryBuiltDist),
213    DirectUrl(DirectUrlBuiltDist),
214    Path(PathBuiltDist),
215    GitPath(GitPathBuiltDist),
216}
217
218/// A source distribution, with its six possible origins (index, URL, Git directory, Git path,
219/// path, or directory).
220#[derive(Debug, Clone, Hash, PartialEq, Eq)]
221pub enum SourceDist {
222    Registry(RegistrySourceDist),
223    DirectUrl(DirectUrlSourceDist),
224    GitDirectory(GitDirectorySourceDist),
225    GitPath(GitPathSourceDist),
226    Path(PathSourceDist),
227    Directory(DirectorySourceDist),
228}
229
230/// A built distribution (wheel) that exists in a registry, like `PyPI`.
231#[derive(Debug, Clone, Hash, PartialEq, Eq)]
232pub struct RegistryBuiltWheel {
233    pub filename: WheelFilename,
234    pub file: Box<File>,
235    pub index: IndexUrl,
236    /// Whether the recorded size must be validated when the wheel is downloaded.
237    pub size_is_authoritative: bool,
238}
239
240/// A built distribution (wheel) that exists in a registry, like `PyPI`.
241#[derive(Debug, Clone, Hash, PartialEq, Eq)]
242pub struct RegistryBuiltDist {
243    /// All wheels associated with this distribution. It is guaranteed
244    /// that there is at least one wheel.
245    pub wheels: Vec<RegistryBuiltWheel>,
246    /// The "best" wheel selected based on the current wheel tag
247    /// environment.
248    ///
249    /// This is guaranteed to point into a valid entry in `wheels`.
250    pub best_wheel_index: usize,
251    /// A source distribution if one exists for this distribution.
252    ///
253    /// It is possible for this to be `None`. For example, when a distribution
254    /// has no source distribution, or if it does have one but isn't compatible
255    /// with the user configuration. (e.g., If `Requires-Python` isn't
256    /// compatible with the installed/target Python versions, or if something
257    /// like `--exclude-newer` was used.)
258    pub sdist: Option<RegistrySourceDist>,
259    // Ideally, this type would have an index URL on it, and the
260    // `RegistryBuiltDist` and `RegistrySourceDist` types would *not* have an
261    // index URL on them. Alas, the --find-links feature makes it technically
262    // possible for the indexes to diverge across wheels/sdists in the same
263    // distribution.
264    //
265    // Note though that at time of writing, when generating a universal lock
266    // file, we require that all index URLs across wheels/sdists for a single
267    // distribution are equivalent.
268}
269
270/// A built distribution (wheel) that exists at an arbitrary URL.
271#[derive(Debug, Clone, Hash, PartialEq, Eq)]
272pub struct DirectUrlBuiltDist {
273    /// We require that wheel urls end in the full wheel filename, e.g.
274    /// `https://example.org/packages/flask-3.0.0-py3-none-any.whl`
275    pub filename: WheelFilename,
276    /// The URL without the subdirectory fragment.
277    pub location: Box<DisplaySafeUrl>,
278    /// The URL as it was provided by the user.
279    pub url: VerbatimUrl,
280    /// The expected size of the archive, if provided by a lockfile.
281    pub size: Option<u64>,
282}
283
284/// A built distribution (wheel) that exists in a local directory.
285#[derive(Debug, Clone, Hash, PartialEq, Eq)]
286pub struct PathBuiltDist {
287    pub filename: WheelFilename,
288    /// The absolute path to the wheel which we use for installing.
289    pub install_path: Box<Path>,
290    /// The URL as it was provided by the user.
291    pub url: VerbatimUrl,
292}
293
294/// A built distribution (wheel) that exists in a Git repository.
295#[derive(Debug, Clone, Hash, PartialEq, Eq)]
296pub struct GitPathBuiltDist {
297    pub filename: WheelFilename,
298    /// The URL without the revision and path fragment.
299    pub git: Box<GitUrl>,
300    /// The path within the Git repository to the distribution which we use for installing.
301    pub install_path: PathBuf,
302    /// The URL as it was provided by the user, including the revision and path fragment.
303    pub url: VerbatimUrl,
304}
305
306/// A source distribution that exists in a registry, like `PyPI`.
307#[derive(Debug, Clone, Hash, PartialEq, Eq)]
308pub struct RegistrySourceDist {
309    pub name: PackageName,
310    pub version: Version,
311    pub file: Box<File>,
312    /// The file extension, e.g. `tar.gz`, `zip`, etc.
313    pub ext: SourceDistExtension,
314    pub index: IndexUrl,
315    /// When an sdist is selected, it may be the case that there were
316    /// available wheels too. There are many reasons why a wheel might not
317    /// have been chosen (maybe none available are compatible with the
318    /// current environment), but we still want to track that they exist. In
319    /// particular, for generating a universal lockfile, we do not want to
320    /// skip emitting wheels to the lockfile just because the host generating
321    /// the lockfile didn't have any compatible wheels available.
322    pub wheels: Vec<RegistryBuiltWheel>,
323    /// Whether the recorded size must be validated when the source distribution is downloaded.
324    pub size_is_authoritative: bool,
325}
326
327/// A source distribution that exists at an arbitrary URL.
328#[derive(Debug, Clone, Hash, PartialEq, Eq)]
329pub struct DirectUrlSourceDist {
330    /// Unlike [`DirectUrlBuiltDist`], we can't require a full filename with a version here, people
331    /// like using e.g. `foo @ https://github.com/org/repo/archive/master.zip`
332    pub name: PackageName,
333    /// The URL without the subdirectory fragment.
334    pub location: Box<DisplaySafeUrl>,
335    /// The subdirectory within the archive in which the source distribution is located.
336    pub subdirectory: Option<Box<Path>>,
337    /// The file extension, e.g. `tar.gz`, `zip`, etc.
338    pub ext: SourceDistExtension,
339    /// The URL as it was provided by the user, including the subdirectory fragment.
340    pub url: VerbatimUrl,
341    /// The expected size of the archive, if provided by a lockfile.
342    pub size: Option<u64>,
343}
344
345/// A source distribution that exists at the root or in a subdirectory of a Git repository.
346#[derive(Debug, Clone, Hash, PartialEq, Eq)]
347pub struct GitDirectorySourceDist {
348    pub name: PackageName,
349    /// The URL without the revision and subdirectory fragment.
350    pub git: Box<GitUrl>,
351    /// The subdirectory within the Git repository in which the source distribution is located.
352    pub subdirectory: Option<Box<Path>>,
353    /// The URL as it was provided by the user, including the revision and subdirectory fragment.
354    pub url: VerbatimUrl,
355}
356
357/// A source distribution that exists in a local archive (e.g., a `.tar.gz` file) within a Git
358/// repository.
359#[derive(Debug, Clone, Hash, PartialEq, Eq)]
360pub struct GitPathSourceDist {
361    pub name: PackageName,
362    /// The URL without the revision and subdirectory fragment.
363    pub git: Box<GitUrl>,
364    /// The path within the Git repository to the distribution which we use for installing.
365    pub install_path: PathBuf,
366    /// The file extension, e.g. `tar.gz`, `zip`, etc.
367    pub ext: SourceDistExtension,
368    /// The URL as it was provided by the user, including the revision and subdirectory fragment.
369    pub url: VerbatimUrl,
370}
371
372/// A source distribution that exists in a local archive (e.g., a `.tar.gz` file).
373#[derive(Debug, Clone, Hash, PartialEq, Eq)]
374pub struct PathSourceDist {
375    pub name: PackageName,
376    pub version: Option<Version>,
377    /// The absolute path to the distribution which we use for installing.
378    pub install_path: Box<Path>,
379    /// The file extension, e.g. `tar.gz`, `zip`, etc.
380    pub ext: SourceDistExtension,
381    /// The URL as it was provided by the user.
382    pub url: VerbatimUrl,
383}
384
385/// A source distribution that exists in a local directory.
386#[derive(Debug, Clone, Hash, PartialEq, Eq)]
387pub struct DirectorySourceDist {
388    pub name: PackageName,
389    /// The absolute path to the distribution which we use for installing.
390    pub install_path: Box<Path>,
391    /// Whether the package should be installed in editable mode.
392    pub editable: Option<bool>,
393    /// Whether the package should be built and installed.
394    pub r#virtual: Option<bool>,
395    /// The URL as it was provided by the user.
396    pub url: VerbatimUrl,
397}
398
399impl Dist {
400    /// A remote built distribution (`.whl`) or source distribution from a `http://` or `https://`
401    /// URL.
402    pub fn from_http_url(
403        name: PackageName,
404        url: VerbatimUrl,
405        location: DisplaySafeUrl,
406        subdirectory: Option<Box<Path>>,
407        ext: DistExtension,
408    ) -> Result<Self, Error> {
409        match ext {
410            DistExtension::Wheel => {
411                // Validate that the name in the wheel matches that of the requirement.
412                let filename = WheelFilename::from_str(&url.filename()?)?;
413                if filename.name != name {
414                    return Err(Error::PackageNameMismatch(
415                        name,
416                        filename.name,
417                        url.verbatim().to_string(),
418                    ));
419                }
420
421                Ok(Self::Built(BuiltDist::DirectUrl(DirectUrlBuiltDist {
422                    filename,
423                    location: Box::new(location),
424                    url,
425                    size: None,
426                })))
427            }
428            DistExtension::Source(ext) => {
429                if !ext.is_pep625_compliant() {
430                    return Err(Error::NotPep625Filename(url.verbatim().to_string()));
431                }
432                Ok(Self::Source(SourceDist::DirectUrl(DirectUrlSourceDist {
433                    name,
434                    location: Box::new(location),
435                    subdirectory,
436                    ext,
437                    url,
438                    size: None,
439                })))
440            }
441        }
442    }
443
444    /// A local built or source distribution from a `file://` URL.
445    pub fn from_file_url(
446        name: PackageName,
447        url: VerbatimUrl,
448        install_path: &Path,
449        ext: DistExtension,
450    ) -> Result<Self, Error> {
451        // Convert to an absolute path.
452        let install_path = path::absolute(install_path)?;
453
454        // Normalize the path.
455        let install_path = normalize_absolute_path(&install_path)?;
456
457        // Validate that the path exists.
458        if !install_path.exists() {
459            return Err(Error::NotFound(url.to_url()));
460        }
461
462        // Determine whether the path represents a built or source distribution.
463        match ext {
464            DistExtension::Wheel => {
465                // Validate that the name in the wheel matches that of the requirement.
466                let filename = install_path
467                    .file_name()
468                    .and_then(OsStr::to_str)
469                    .ok_or_else(|| Error::MissingWheelFilename(install_path.clone()))?;
470                let filename = WheelFilename::from_str(filename)?;
471                if filename.name != name {
472                    return Err(Error::PackageNameMismatch(
473                        name,
474                        filename.name,
475                        url.verbatim().to_string(),
476                    ));
477                }
478                Ok(Self::Built(BuiltDist::Path(PathBuiltDist {
479                    filename,
480                    install_path: install_path.into_boxed_path(),
481                    url,
482                })))
483            }
484            DistExtension::Source(ext) => {
485                if !ext.is_pep625_compliant() {
486                    return Err(Error::NotPep625Filename(url.verbatim().to_string()));
487                }
488
489                // If there is a version in the filename, record it.
490                let version = url
491                    .filename()
492                    .ok()
493                    .and_then(|filename| {
494                        SourceDistFilename::parse(filename.as_ref(), ext, &name).ok()
495                    })
496                    .map(|filename| filename.version);
497
498                Ok(Self::Source(SourceDist::Path(PathSourceDist {
499                    name,
500                    version,
501                    install_path: install_path.into_boxed_path(),
502                    ext,
503                    url,
504                })))
505            }
506        }
507    }
508
509    /// A local source tree from a `file://` URL.
510    pub fn from_directory_url(
511        name: PackageName,
512        url: VerbatimUrl,
513        install_path: &Path,
514        editable: Option<bool>,
515        r#virtual: Option<bool>,
516    ) -> Result<Self, Error> {
517        // Convert to an absolute path.
518        let install_path = path::absolute(install_path)?;
519
520        // Normalize the path.
521        let install_path = normalize_absolute_path(&install_path)?;
522
523        // Validate that the path exists.
524        if !install_path.exists() {
525            return Err(Error::NotFound(url.to_url()));
526        }
527
528        // Determine whether the path represents an archive or a directory.
529        Ok(Self::Source(SourceDist::Directory(DirectorySourceDist {
530            name,
531            install_path: install_path.into_boxed_path(),
532            editable,
533            r#virtual,
534            url,
535        })))
536    }
537
538    /// Create a [`Dist`] for a source tree within a Git repository (i.e., a `git+https://` or `git+ssh://` URL).
539    pub fn from_git_directory_url(
540        name: PackageName,
541        url: VerbatimUrl,
542        git: GitUrl,
543        subdirectory: Option<Box<Path>>,
544    ) -> Result<Self, Error> {
545        Ok(Self::Source(SourceDist::GitDirectory(
546            GitDirectorySourceDist {
547                name,
548                git: Box::new(git),
549                subdirectory,
550                url,
551            },
552        )))
553    }
554
555    /// Create a [`Dist`] for a source archive within a Git repository (i.e., a `git+https://` or `git+ssh://` URL).
556    pub fn from_git_path_url(
557        name: PackageName,
558        url: VerbatimUrl,
559        git: GitUrl,
560        install_path: PathBuf,
561        ext: DistExtension,
562    ) -> Result<Self, Error> {
563        match ext {
564            DistExtension::Wheel => {
565                // Validate that the name in the wheel matches that of the requirement.
566                let filename = install_path
567                    .file_name()
568                    .and_then(OsStr::to_str)
569                    .ok_or_else(|| Error::MissingWheelFilename(install_path.clone()))?;
570                let filename = WheelFilename::from_str(filename)?;
571                if filename.name != name {
572                    return Err(Error::PackageNameMismatch(
573                        name,
574                        filename.name,
575                        url.verbatim().to_string(),
576                    ));
577                }
578
579                Ok(Self::Built(BuiltDist::GitPath(GitPathBuiltDist {
580                    filename,
581                    git: Box::new(git),
582                    install_path,
583                    url,
584                })))
585            }
586            DistExtension::Source(ext) => {
587                Ok(Self::Source(SourceDist::GitPath(GitPathSourceDist {
588                    name,
589                    git: Box::new(git),
590                    install_path,
591                    ext,
592                    url,
593                })))
594            }
595        }
596    }
597
598    /// Create a [`Dist`] for a URL-based distribution.
599    pub fn from_url(name: PackageName, url: VerbatimParsedUrl) -> Result<Self, Error> {
600        match url.parsed_url {
601            ParsedUrl::Archive(archive) => Self::from_http_url(
602                name,
603                url.verbatim,
604                archive.url,
605                archive.subdirectory,
606                archive.ext,
607            ),
608            ParsedUrl::Path(file) => {
609                Self::from_file_url(name, url.verbatim, &file.install_path, file.ext)
610            }
611            ParsedUrl::Directory(directory) => Self::from_directory_url(
612                name,
613                url.verbatim,
614                &directory.install_path,
615                directory.editable,
616                directory.r#virtual,
617            ),
618            ParsedUrl::GitDirectory(git) => {
619                Self::from_git_directory_url(name, url.verbatim, git.url, git.subdirectory)
620            }
621            ParsedUrl::GitPath(git) => {
622                Self::from_git_path_url(name, url.verbatim, git.url, git.install_path, git.ext)
623            }
624        }
625    }
626
627    /// Return true if the distribution is editable.
628    fn is_editable(&self) -> bool {
629        match self {
630            Self::Source(dist) => dist.is_editable(),
631            Self::Built(_) => false,
632        }
633    }
634
635    /// Return true if the distribution refers to a local file or directory.
636    fn is_local(&self) -> bool {
637        match self {
638            Self::Source(dist) => dist.is_local(),
639            Self::Built(dist) => dist.is_local(),
640        }
641    }
642
643    /// Returns the [`IndexUrl`], if the distribution is from a registry.
644    pub fn index(&self) -> Option<&IndexUrl> {
645        match self {
646            Self::Built(dist) => dist.index(),
647            Self::Source(dist) => dist.index(),
648        }
649    }
650
651    /// Returns the [`File`] instance, if this dist is from a registry with simple json api support
652    pub fn file(&self) -> Option<&File> {
653        match self {
654            Self::Built(built) => built.file(),
655            Self::Source(source) => source.file(),
656        }
657    }
658
659    /// Return the source tree of the distribution, if available.
660    pub fn source_tree(&self) -> Option<&Path> {
661        match self {
662            Self::Built { .. } => None,
663            Self::Source(source) => source.source_tree(),
664        }
665    }
666
667    /// Returns the version of the distribution, if it is known.
668    pub fn version(&self) -> Option<&Version> {
669        match self {
670            Self::Built(wheel) => Some(wheel.version()),
671            Self::Source(source_dist) => source_dist.version(),
672        }
673    }
674}
675
676impl<'a> From<&'a Dist> for DistRef<'a> {
677    fn from(dist: &'a Dist) -> Self {
678        match dist {
679            Dist::Built(built) => DistRef::Built(built),
680            Dist::Source(source) => DistRef::Source(source),
681        }
682    }
683}
684
685impl<'a> From<&'a SourceDist> for DistRef<'a> {
686    fn from(dist: &'a SourceDist) -> Self {
687        DistRef::Source(dist)
688    }
689}
690
691impl<'a> From<&'a BuiltDist> for DistRef<'a> {
692    fn from(dist: &'a BuiltDist) -> Self {
693        DistRef::Built(dist)
694    }
695}
696
697impl BuiltDist {
698    /// Return true if the distribution refers to a local file or directory.
699    fn is_local(&self) -> bool {
700        matches!(self, Self::Path(_))
701    }
702
703    /// Returns the [`IndexUrl`], if the distribution is from a registry.
704    pub fn index(&self) -> Option<&IndexUrl> {
705        match self {
706            Self::Registry(registry) => Some(&registry.best_wheel().index),
707            Self::DirectUrl(_) => None,
708            Self::Path(_) => None,
709            Self::GitPath(_) => None,
710        }
711    }
712
713    /// Returns the [`File`] instance, if this distribution is from a registry.
714    fn file(&self) -> Option<&File> {
715        match self {
716            Self::Registry(registry) => Some(&registry.best_wheel().file),
717            Self::DirectUrl(_) | Self::Path(_) | Self::GitPath(_) => None,
718        }
719    }
720
721    pub fn version(&self) -> &Version {
722        match self {
723            Self::Registry(wheels) => &wheels.best_wheel().filename.version,
724            Self::DirectUrl(wheel) => &wheel.filename.version,
725            Self::Path(wheel) => &wheel.filename.version,
726            Self::GitPath(wheel) => &wheel.filename.version,
727        }
728    }
729}
730
731impl SourceDist {
732    /// Returns the [`IndexUrl`], if the distribution is from a registry.
733    fn index(&self) -> Option<&IndexUrl> {
734        match self {
735            Self::Registry(registry) => Some(&registry.index),
736            Self::DirectUrl(_)
737            | Self::GitPath(_)
738            | Self::GitDirectory(_)
739            | Self::Path(_)
740            | Self::Directory(_) => None,
741        }
742    }
743
744    /// Returns the [`File`] instance, if this dist is from a registry with simple json api support
745    fn file(&self) -> Option<&File> {
746        match self {
747            Self::Registry(registry) => Some(&registry.file),
748            Self::DirectUrl(_)
749            | Self::GitPath(_)
750            | Self::GitDirectory(_)
751            | Self::Path(_)
752            | Self::Directory(_) => None,
753        }
754    }
755
756    /// Returns the [`Version`] of the distribution, if it is known.
757    pub fn version(&self) -> Option<&Version> {
758        match self {
759            Self::Registry(source_dist) => Some(&source_dist.version),
760            Self::DirectUrl(_)
761            | Self::GitPath(_)
762            | Self::GitDirectory(_)
763            | Self::Path(_)
764            | Self::Directory(_) => None,
765        }
766    }
767
768    /// Returns `true` if the distribution is editable.
769    pub fn is_editable(&self) -> bool {
770        match self {
771            Self::Directory(DirectorySourceDist { editable, .. }) => editable.unwrap_or(false),
772            _ => false,
773        }
774    }
775
776    /// Returns `true` if the distribution is virtual.
777    pub fn is_virtual(&self) -> bool {
778        match self {
779            Self::Directory(DirectorySourceDist { r#virtual, .. }) => r#virtual.unwrap_or(false),
780            _ => false,
781        }
782    }
783
784    /// Returns `true` if the distribution refers to a local file or directory.
785    fn is_local(&self) -> bool {
786        matches!(self, Self::Directory(_) | Self::Path(_))
787    }
788
789    /// Returns the path to the source distribution, if it's a local distribution.
790    pub fn as_path(&self) -> Option<&Path> {
791        match self {
792            Self::Path(dist) => Some(&dist.install_path),
793            Self::Directory(dist) => Some(&dist.install_path),
794            _ => None,
795        }
796    }
797
798    /// Returns the source tree of the distribution, if available.
799    fn source_tree(&self) -> Option<&Path> {
800        match self {
801            Self::Directory(dist) => Some(&dist.install_path),
802            _ => None,
803        }
804    }
805}
806
807impl RegistryBuiltDist {
808    /// Returns the best or "most compatible" wheel in this distribution.
809    pub fn best_wheel(&self) -> &RegistryBuiltWheel {
810        &self.wheels[self.best_wheel_index]
811    }
812}
813
814impl DirectUrlBuiltDist {
815    /// Return the [`ParsedUrl`] for the distribution.
816    pub fn to_parsed_url(&self) -> ParsedUrl {
817        ParsedUrl::Archive(ParsedArchiveUrl::from_source(
818            (*self.location).clone(),
819            None,
820            DistExtension::Wheel,
821        ))
822    }
823}
824
825impl PathBuiltDist {
826    /// Return the [`ParsedUrl`] for the distribution.
827    pub fn to_parsed_url(&self) -> ParsedUrl {
828        ParsedUrl::Path(ParsedPathUrl::from_source(
829            self.install_path.clone(),
830            DistExtension::Wheel,
831            self.url.to_url(),
832        ))
833    }
834}
835
836impl PathSourceDist {
837    /// Return the [`ParsedUrl`] for the distribution.
838    pub fn to_parsed_url(&self) -> ParsedUrl {
839        ParsedUrl::Path(ParsedPathUrl::from_source(
840            self.install_path.clone(),
841            DistExtension::Source(self.ext),
842            self.url.to_url(),
843        ))
844    }
845}
846
847impl DirectUrlSourceDist {
848    /// Return the [`ParsedUrl`] for the distribution.
849    pub fn to_parsed_url(&self) -> ParsedUrl {
850        ParsedUrl::Archive(ParsedArchiveUrl::from_source(
851            (*self.location).clone(),
852            self.subdirectory.clone(),
853            DistExtension::Source(self.ext),
854        ))
855    }
856}
857
858impl GitDirectorySourceDist {
859    /// Return the [`ParsedUrl`] for the distribution.
860    pub fn to_parsed_url(&self) -> ParsedUrl {
861        ParsedUrl::GitDirectory(ParsedGitDirectoryUrl::from_source(
862            (*self.git).clone(),
863            self.subdirectory.clone(),
864        ))
865    }
866}
867
868impl GitPathBuiltDist {
869    /// Return the [`ParsedUrl`] for the distribution.
870    pub fn to_parsed_url(&self) -> ParsedUrl {
871        ParsedUrl::GitPath(ParsedGitPathUrl::from_source(
872            (*self.git).clone(),
873            self.install_path.clone(),
874            DistExtension::Wheel,
875        ))
876    }
877}
878
879impl GitPathSourceDist {
880    /// Return the [`ParsedUrl`] for the distribution.
881    pub fn to_parsed_url(&self) -> ParsedUrl {
882        ParsedUrl::GitPath(ParsedGitPathUrl::from_source(
883            (*self.git).clone(),
884            self.install_path.clone(),
885            DistExtension::Source(self.ext),
886        ))
887    }
888}
889
890impl DirectorySourceDist {
891    /// Return the [`ParsedUrl`] for the distribution.
892    pub fn to_parsed_url(&self) -> ParsedUrl {
893        ParsedUrl::Directory(ParsedDirectoryUrl::from_source(
894            self.install_path.clone(),
895            self.editable,
896            self.r#virtual,
897            self.url.to_url(),
898        ))
899    }
900}
901
902impl Name for RegistryBuiltWheel {
903    fn name(&self) -> &PackageName {
904        &self.filename.name
905    }
906}
907
908impl Name for RegistryBuiltDist {
909    fn name(&self) -> &PackageName {
910        self.best_wheel().name()
911    }
912}
913
914impl Name for DirectUrlBuiltDist {
915    fn name(&self) -> &PackageName {
916        &self.filename.name
917    }
918}
919
920impl Name for PathBuiltDist {
921    fn name(&self) -> &PackageName {
922        &self.filename.name
923    }
924}
925
926impl Name for GitPathBuiltDist {
927    fn name(&self) -> &PackageName {
928        &self.filename.name
929    }
930}
931
932impl Name for RegistrySourceDist {
933    fn name(&self) -> &PackageName {
934        &self.name
935    }
936}
937
938impl Name for DirectUrlSourceDist {
939    fn name(&self) -> &PackageName {
940        &self.name
941    }
942}
943
944impl Name for GitPathSourceDist {
945    fn name(&self) -> &PackageName {
946        &self.name
947    }
948}
949
950impl Name for GitDirectorySourceDist {
951    fn name(&self) -> &PackageName {
952        &self.name
953    }
954}
955
956impl Name for PathSourceDist {
957    fn name(&self) -> &PackageName {
958        &self.name
959    }
960}
961
962impl Name for DirectorySourceDist {
963    fn name(&self) -> &PackageName {
964        &self.name
965    }
966}
967
968impl Name for SourceDist {
969    fn name(&self) -> &PackageName {
970        match self {
971            Self::Registry(dist) => dist.name(),
972            Self::DirectUrl(dist) => dist.name(),
973            Self::GitPath(dist) => dist.name(),
974            Self::GitDirectory(dist) => dist.name(),
975            Self::Path(dist) => dist.name(),
976            Self::Directory(dist) => dist.name(),
977        }
978    }
979}
980
981impl Name for BuiltDist {
982    fn name(&self) -> &PackageName {
983        match self {
984            Self::Registry(dist) => dist.name(),
985            Self::DirectUrl(dist) => dist.name(),
986            Self::Path(dist) => dist.name(),
987            Self::GitPath(dist) => dist.name(),
988        }
989    }
990}
991
992impl Name for Dist {
993    fn name(&self) -> &PackageName {
994        match self {
995            Self::Built(dist) => dist.name(),
996            Self::Source(dist) => dist.name(),
997        }
998    }
999}
1000
1001impl Name for CompatibleDist<'_> {
1002    fn name(&self) -> &PackageName {
1003        match self {
1004            Self::InstalledDist(dist) => dist.name(),
1005            Self::SourceDist {
1006                sdist,
1007                prioritized: _,
1008            } => sdist.name(),
1009            Self::CompatibleWheel {
1010                wheel,
1011                priority: _,
1012                prioritized: _,
1013            } => wheel.name(),
1014            Self::IncompatibleWheel {
1015                sdist,
1016                wheel: _,
1017                prioritized: _,
1018            } => sdist.name(),
1019        }
1020    }
1021}
1022
1023impl DistributionMetadata for RegistryBuiltWheel {
1024    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1025        VersionOrUrlRef::Version(&self.filename.version)
1026    }
1027}
1028
1029impl DistributionMetadata for RegistryBuiltDist {
1030    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1031        self.best_wheel().version_or_url()
1032    }
1033}
1034
1035impl DistributionMetadata for DirectUrlBuiltDist {
1036    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1037        VersionOrUrlRef::Url(&self.url)
1038    }
1039
1040    fn version_id(&self) -> VersionId {
1041        VersionId::from_archive(self.location.as_ref().clone(), None)
1042    }
1043}
1044
1045impl DistributionMetadata for PathBuiltDist {
1046    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1047        VersionOrUrlRef::Url(&self.url)
1048    }
1049
1050    fn version_id(&self) -> VersionId {
1051        VersionId::from_path(self.install_path.as_ref())
1052    }
1053}
1054
1055impl DistributionMetadata for GitPathBuiltDist {
1056    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1057        VersionOrUrlRef::Url(&self.url)
1058    }
1059}
1060
1061impl DistributionMetadata for RegistrySourceDist {
1062    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1063        VersionOrUrlRef::Version(&self.version)
1064    }
1065}
1066
1067impl DistributionMetadata for DirectUrlSourceDist {
1068    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1069        VersionOrUrlRef::Url(&self.url)
1070    }
1071
1072    fn version_id(&self) -> VersionId {
1073        VersionId::from_archive(
1074            self.location.as_ref().clone(),
1075            self.subdirectory.clone().map(Path::into_path_buf),
1076        )
1077    }
1078}
1079
1080impl DistributionMetadata for GitPathSourceDist {
1081    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1082        VersionOrUrlRef::Url(&self.url)
1083    }
1084
1085    fn version_id(&self) -> VersionId {
1086        VersionId::from_git(self.git.as_ref(), Some(&self.install_path))
1087    }
1088}
1089
1090impl DistributionMetadata for GitDirectorySourceDist {
1091    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1092        VersionOrUrlRef::Url(&self.url)
1093    }
1094
1095    fn version_id(&self) -> VersionId {
1096        VersionId::from_git(self.git.as_ref(), self.subdirectory.as_deref())
1097    }
1098}
1099
1100impl DistributionMetadata for PathSourceDist {
1101    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1102        VersionOrUrlRef::Url(&self.url)
1103    }
1104
1105    fn version_id(&self) -> VersionId {
1106        VersionId::from_path(self.install_path.as_ref())
1107    }
1108}
1109
1110impl DistributionMetadata for DirectorySourceDist {
1111    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1112        VersionOrUrlRef::Url(&self.url)
1113    }
1114
1115    fn version_id(&self) -> VersionId {
1116        VersionId::from_directory(self.install_path.as_ref())
1117    }
1118}
1119
1120impl DistributionMetadata for SourceDist {
1121    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1122        match self {
1123            Self::Registry(dist) => dist.version_or_url(),
1124            Self::DirectUrl(dist) => dist.version_or_url(),
1125            Self::GitPath(dist) => dist.version_or_url(),
1126            Self::GitDirectory(dist) => dist.version_or_url(),
1127            Self::Path(dist) => dist.version_or_url(),
1128            Self::Directory(dist) => dist.version_or_url(),
1129        }
1130    }
1131
1132    fn version_id(&self) -> VersionId {
1133        match self {
1134            Self::Registry(dist) => dist.version_id(),
1135            Self::DirectUrl(dist) => dist.version_id(),
1136            Self::GitPath(dist) => dist.version_id(),
1137            Self::GitDirectory(dist) => dist.version_id(),
1138            Self::Path(dist) => dist.version_id(),
1139            Self::Directory(dist) => dist.version_id(),
1140        }
1141    }
1142}
1143
1144impl DistributionMetadata for BuiltDist {
1145    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1146        match self {
1147            Self::Registry(dist) => dist.version_or_url(),
1148            Self::DirectUrl(dist) => dist.version_or_url(),
1149            Self::Path(dist) => dist.version_or_url(),
1150            Self::GitPath(dist) => dist.version_or_url(),
1151        }
1152    }
1153
1154    fn version_id(&self) -> VersionId {
1155        match self {
1156            Self::Registry(dist) => dist.version_id(),
1157            Self::DirectUrl(dist) => dist.version_id(),
1158            Self::Path(dist) => dist.version_id(),
1159            Self::GitPath(dist) => dist.version_id(),
1160        }
1161    }
1162}
1163
1164impl DistributionMetadata for Dist {
1165    fn version_or_url(&self) -> VersionOrUrlRef<'_> {
1166        match self {
1167            Self::Built(dist) => dist.version_or_url(),
1168            Self::Source(dist) => dist.version_or_url(),
1169        }
1170    }
1171
1172    fn version_id(&self) -> VersionId {
1173        match self {
1174            Self::Built(dist) => dist.version_id(),
1175            Self::Source(dist) => dist.version_id(),
1176        }
1177    }
1178}
1179
1180impl RemoteSource for File {
1181    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1182        Ok(Cow::Borrowed(&self.filename))
1183    }
1184
1185    fn size(&self) -> Option<u64> {
1186        self.size
1187    }
1188}
1189
1190impl RemoteSource for Url {
1191    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1192        // Identify the last segment of the URL as the filename.
1193        let mut path_segments = self
1194            .path_segments()
1195            .ok_or_else(|| Error::MissingPathSegments(self.to_string()))?;
1196
1197        // This is guaranteed by the contract of `Url::path_segments`.
1198        let last = path_segments
1199            .next_back()
1200            .expect("path segments is non-empty");
1201
1202        // Decode the filename, which may be percent-encoded.
1203        let filename = percent_encoding::percent_decode_str(last).decode_utf8()?;
1204
1205        Ok(filename)
1206    }
1207
1208    fn size(&self) -> Option<u64> {
1209        None
1210    }
1211}
1212
1213impl RemoteSource for UrlString {
1214    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1215        // Take the last segment, stripping any query or fragment.
1216        let last = self
1217            .base_str()
1218            .split('/')
1219            .next_back()
1220            .ok_or_else(|| Error::MissingPathSegments(self.to_string()))?;
1221
1222        // Decode the filename, which may be percent-encoded.
1223        let filename = percent_encoding::percent_decode_str(last).decode_utf8()?;
1224
1225        Ok(filename)
1226    }
1227
1228    fn size(&self) -> Option<u64> {
1229        None
1230    }
1231}
1232
1233impl RemoteSource for RegistryBuiltWheel {
1234    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1235        self.file.filename()
1236    }
1237
1238    fn size(&self) -> Option<u64> {
1239        self.file.size()
1240    }
1241}
1242
1243impl RemoteSource for RegistryBuiltDist {
1244    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1245        self.best_wheel().filename()
1246    }
1247
1248    fn size(&self) -> Option<u64> {
1249        self.best_wheel().size()
1250    }
1251}
1252
1253impl RemoteSource for RegistrySourceDist {
1254    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1255        self.file.filename()
1256    }
1257
1258    fn size(&self) -> Option<u64> {
1259        self.file.size()
1260    }
1261}
1262
1263impl RemoteSource for DirectUrlBuiltDist {
1264    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1265        self.url.filename()
1266    }
1267
1268    fn size(&self) -> Option<u64> {
1269        self.size
1270    }
1271}
1272
1273impl RemoteSource for DirectUrlSourceDist {
1274    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1275        self.url.filename()
1276    }
1277
1278    fn size(&self) -> Option<u64> {
1279        self.size
1280    }
1281}
1282
1283impl RemoteSource for GitPathSourceDist {
1284    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1285        // The filename is the last segment of the URL, before any `@`.
1286        match self.url.filename()? {
1287            Cow::Borrowed(filename) => {
1288                if let Some((_, filename)) = filename.rsplit_once('@') {
1289                    Ok(Cow::Borrowed(filename))
1290                } else {
1291                    Ok(Cow::Borrowed(filename))
1292                }
1293            }
1294            Cow::Owned(filename) => {
1295                if let Some((_, filename)) = filename.rsplit_once('@') {
1296                    Ok(Cow::Owned(filename.to_owned()))
1297                } else {
1298                    Ok(Cow::Owned(filename))
1299                }
1300            }
1301        }
1302    }
1303
1304    fn size(&self) -> Option<u64> {
1305        self.url.size()
1306    }
1307}
1308
1309impl RemoteSource for GitDirectorySourceDist {
1310    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1311        // The filename is the last segment of the URL, before any `@`.
1312        match self.url.filename()? {
1313            Cow::Borrowed(filename) => {
1314                if let Some((_, filename)) = filename.rsplit_once('@') {
1315                    Ok(Cow::Borrowed(filename))
1316                } else {
1317                    Ok(Cow::Borrowed(filename))
1318                }
1319            }
1320            Cow::Owned(filename) => {
1321                if let Some((_, filename)) = filename.rsplit_once('@') {
1322                    Ok(Cow::Owned(filename.to_owned()))
1323                } else {
1324                    Ok(Cow::Owned(filename))
1325                }
1326            }
1327        }
1328    }
1329
1330    fn size(&self) -> Option<u64> {
1331        self.url.size()
1332    }
1333}
1334
1335impl RemoteSource for PathBuiltDist {
1336    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1337        self.url.filename()
1338    }
1339
1340    fn size(&self) -> Option<u64> {
1341        self.url.size()
1342    }
1343}
1344
1345impl RemoteSource for GitPathBuiltDist {
1346    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1347        self.url.filename()
1348    }
1349
1350    fn size(&self) -> Option<u64> {
1351        self.url.size()
1352    }
1353}
1354
1355impl RemoteSource for PathSourceDist {
1356    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1357        self.url.filename()
1358    }
1359
1360    fn size(&self) -> Option<u64> {
1361        self.url.size()
1362    }
1363}
1364
1365impl RemoteSource for DirectorySourceDist {
1366    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1367        self.url.filename()
1368    }
1369
1370    fn size(&self) -> Option<u64> {
1371        self.url.size()
1372    }
1373}
1374
1375impl RemoteSource for SourceDist {
1376    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1377        match self {
1378            Self::Registry(dist) => dist.filename(),
1379            Self::DirectUrl(dist) => dist.filename(),
1380            Self::GitPath(dist) => dist.filename(),
1381            Self::GitDirectory(dist) => dist.filename(),
1382            Self::Path(dist) => dist.filename(),
1383            Self::Directory(dist) => dist.filename(),
1384        }
1385    }
1386
1387    fn size(&self) -> Option<u64> {
1388        match self {
1389            Self::Registry(dist) => dist.size(),
1390            Self::DirectUrl(dist) => dist.size(),
1391            Self::GitPath(dist) => dist.size(),
1392            Self::GitDirectory(dist) => dist.size(),
1393            Self::Path(dist) => dist.size(),
1394            Self::Directory(dist) => dist.size(),
1395        }
1396    }
1397}
1398
1399impl RemoteSource for BuiltDist {
1400    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1401        match self {
1402            Self::Registry(dist) => dist.filename(),
1403            Self::DirectUrl(dist) => dist.filename(),
1404            Self::Path(dist) => dist.filename(),
1405            Self::GitPath(dist) => dist.filename(),
1406        }
1407    }
1408
1409    fn size(&self) -> Option<u64> {
1410        match self {
1411            Self::Registry(dist) => dist.size(),
1412            Self::DirectUrl(dist) => dist.size(),
1413            Self::Path(dist) => dist.size(),
1414            Self::GitPath(dist) => dist.size(),
1415        }
1416    }
1417}
1418
1419impl RemoteSource for Dist {
1420    fn filename(&self) -> Result<Cow<'_, str>, Error> {
1421        match self {
1422            Self::Built(dist) => dist.filename(),
1423            Self::Source(dist) => dist.filename(),
1424        }
1425    }
1426
1427    fn size(&self) -> Option<u64> {
1428        match self {
1429            Self::Built(dist) => dist.size(),
1430            Self::Source(dist) => dist.size(),
1431        }
1432    }
1433}
1434
1435impl Identifier for DisplaySafeUrl {
1436    fn distribution_id(&self) -> DistributionId {
1437        DistributionId::Url(uv_cache_key::CanonicalUrl::new(self.clone()))
1438    }
1439
1440    fn resource_id(&self) -> ResourceId {
1441        ResourceId::Url(uv_cache_key::RepositoryUrl::new(self.clone()))
1442    }
1443}
1444
1445impl Identifier for File {
1446    fn distribution_id(&self) -> DistributionId {
1447        self.hashes
1448            .first()
1449            .cloned()
1450            .map(DistributionId::Digest)
1451            .unwrap_or_else(|| self.url.distribution_id())
1452    }
1453
1454    fn resource_id(&self) -> ResourceId {
1455        self.hashes
1456            .first()
1457            .cloned()
1458            .map(ResourceId::Digest)
1459            .unwrap_or_else(|| self.url.resource_id())
1460    }
1461}
1462
1463impl Identifier for Path {
1464    fn distribution_id(&self) -> DistributionId {
1465        DistributionId::PathBuf(self.to_path_buf())
1466    }
1467
1468    fn resource_id(&self) -> ResourceId {
1469        ResourceId::PathBuf(self.to_path_buf())
1470    }
1471}
1472
1473impl Identifier for FileLocation {
1474    fn distribution_id(&self) -> DistributionId {
1475        match self {
1476            Self::RelativeUrl(base, url) => {
1477                DistributionId::RelativeUrl(base.to_string(), url.to_string())
1478            }
1479            Self::AbsoluteUrl(url) => DistributionId::AbsoluteUrl(url.to_string()),
1480        }
1481    }
1482
1483    fn resource_id(&self) -> ResourceId {
1484        match self {
1485            Self::RelativeUrl(base, url) => {
1486                ResourceId::RelativeUrl(base.to_string(), url.to_string())
1487            }
1488            Self::AbsoluteUrl(url) => ResourceId::AbsoluteUrl(url.to_string()),
1489        }
1490    }
1491}
1492
1493impl Identifier for RegistryBuiltWheel {
1494    fn distribution_id(&self) -> DistributionId {
1495        self.file.distribution_id()
1496    }
1497
1498    fn resource_id(&self) -> ResourceId {
1499        self.file.resource_id()
1500    }
1501}
1502
1503impl Identifier for RegistryBuiltDist {
1504    fn distribution_id(&self) -> DistributionId {
1505        self.best_wheel().distribution_id()
1506    }
1507
1508    fn resource_id(&self) -> ResourceId {
1509        self.best_wheel().resource_id()
1510    }
1511}
1512
1513impl Identifier for RegistrySourceDist {
1514    fn distribution_id(&self) -> DistributionId {
1515        self.file.distribution_id()
1516    }
1517
1518    fn resource_id(&self) -> ResourceId {
1519        self.file.resource_id()
1520    }
1521}
1522
1523impl Identifier for DirectUrlBuiltDist {
1524    fn distribution_id(&self) -> DistributionId {
1525        self.url.distribution_id()
1526    }
1527
1528    fn resource_id(&self) -> ResourceId {
1529        self.url.resource_id()
1530    }
1531}
1532
1533impl Identifier for DirectUrlSourceDist {
1534    fn distribution_id(&self) -> DistributionId {
1535        self.url.distribution_id()
1536    }
1537
1538    fn resource_id(&self) -> ResourceId {
1539        self.url.resource_id()
1540    }
1541}
1542
1543impl Identifier for PathBuiltDist {
1544    fn distribution_id(&self) -> DistributionId {
1545        self.url.distribution_id()
1546    }
1547
1548    fn resource_id(&self) -> ResourceId {
1549        self.url.resource_id()
1550    }
1551}
1552
1553impl Identifier for GitPathBuiltDist {
1554    fn distribution_id(&self) -> DistributionId {
1555        self.url.distribution_id()
1556    }
1557
1558    fn resource_id(&self) -> ResourceId {
1559        self.url.resource_id()
1560    }
1561}
1562
1563impl Identifier for PathSourceDist {
1564    fn distribution_id(&self) -> DistributionId {
1565        self.url.distribution_id()
1566    }
1567
1568    fn resource_id(&self) -> ResourceId {
1569        self.url.resource_id()
1570    }
1571}
1572
1573impl Identifier for DirectorySourceDist {
1574    fn distribution_id(&self) -> DistributionId {
1575        self.url.distribution_id()
1576    }
1577
1578    fn resource_id(&self) -> ResourceId {
1579        self.url.resource_id()
1580    }
1581}
1582
1583impl Identifier for GitPathSourceDist {
1584    fn distribution_id(&self) -> DistributionId {
1585        self.url.distribution_id()
1586    }
1587
1588    fn resource_id(&self) -> ResourceId {
1589        self.url.resource_id()
1590    }
1591}
1592
1593impl Identifier for GitDirectorySourceDist {
1594    fn distribution_id(&self) -> DistributionId {
1595        self.url.distribution_id()
1596    }
1597
1598    fn resource_id(&self) -> ResourceId {
1599        self.url.resource_id()
1600    }
1601}
1602
1603impl Identifier for SourceDist {
1604    fn distribution_id(&self) -> DistributionId {
1605        match self {
1606            Self::Registry(dist) => dist.distribution_id(),
1607            Self::DirectUrl(dist) => dist.distribution_id(),
1608            Self::GitPath(dist) => dist.distribution_id(),
1609            Self::GitDirectory(dist) => dist.distribution_id(),
1610            Self::Path(dist) => dist.distribution_id(),
1611            Self::Directory(dist) => dist.distribution_id(),
1612        }
1613    }
1614
1615    fn resource_id(&self) -> ResourceId {
1616        match self {
1617            Self::Registry(dist) => dist.resource_id(),
1618            Self::DirectUrl(dist) => dist.resource_id(),
1619            Self::GitPath(dist) => dist.resource_id(),
1620            Self::GitDirectory(dist) => dist.resource_id(),
1621            Self::Path(dist) => dist.resource_id(),
1622            Self::Directory(dist) => dist.resource_id(),
1623        }
1624    }
1625}
1626
1627impl Identifier for BuiltDist {
1628    fn distribution_id(&self) -> DistributionId {
1629        match self {
1630            Self::Registry(dist) => dist.distribution_id(),
1631            Self::DirectUrl(dist) => dist.distribution_id(),
1632            Self::Path(dist) => dist.distribution_id(),
1633            Self::GitPath(dist) => dist.distribution_id(),
1634        }
1635    }
1636
1637    fn resource_id(&self) -> ResourceId {
1638        match self {
1639            Self::Registry(dist) => dist.resource_id(),
1640            Self::DirectUrl(dist) => dist.resource_id(),
1641            Self::Path(dist) => dist.resource_id(),
1642            Self::GitPath(dist) => dist.resource_id(),
1643        }
1644    }
1645}
1646
1647impl Identifier for InstalledDist {
1648    fn distribution_id(&self) -> DistributionId {
1649        self.install_path().distribution_id()
1650    }
1651
1652    fn resource_id(&self) -> ResourceId {
1653        self.install_path().resource_id()
1654    }
1655}
1656
1657impl Identifier for Dist {
1658    fn distribution_id(&self) -> DistributionId {
1659        match self {
1660            Self::Built(dist) => dist.distribution_id(),
1661            Self::Source(dist) => dist.distribution_id(),
1662        }
1663    }
1664
1665    fn resource_id(&self) -> ResourceId {
1666        match self {
1667            Self::Built(dist) => dist.resource_id(),
1668            Self::Source(dist) => dist.resource_id(),
1669        }
1670    }
1671}
1672
1673impl Identifier for DirectSourceUrl<'_> {
1674    fn distribution_id(&self) -> DistributionId {
1675        self.url.distribution_id()
1676    }
1677
1678    fn resource_id(&self) -> ResourceId {
1679        self.url.resource_id()
1680    }
1681}
1682
1683impl Identifier for GitDirectorySourceUrl<'_> {
1684    fn distribution_id(&self) -> DistributionId {
1685        self.url.distribution_id()
1686    }
1687
1688    fn resource_id(&self) -> ResourceId {
1689        self.url.resource_id()
1690    }
1691}
1692
1693impl Identifier for GitPathSourceUrl<'_> {
1694    fn distribution_id(&self) -> DistributionId {
1695        self.url.distribution_id()
1696    }
1697
1698    fn resource_id(&self) -> ResourceId {
1699        self.url.resource_id()
1700    }
1701}
1702
1703impl Identifier for PathSourceUrl<'_> {
1704    fn distribution_id(&self) -> DistributionId {
1705        self.url.distribution_id()
1706    }
1707
1708    fn resource_id(&self) -> ResourceId {
1709        self.url.resource_id()
1710    }
1711}
1712
1713impl Identifier for DirectorySourceUrl<'_> {
1714    fn distribution_id(&self) -> DistributionId {
1715        self.url.distribution_id()
1716    }
1717
1718    fn resource_id(&self) -> ResourceId {
1719        self.url.resource_id()
1720    }
1721}
1722
1723impl Identifier for SourceUrl<'_> {
1724    fn distribution_id(&self) -> DistributionId {
1725        match self {
1726            Self::Direct(url) => url.distribution_id(),
1727            Self::GitDirectory(url) => url.distribution_id(),
1728            Self::GitPath(url) => url.distribution_id(),
1729            Self::Path(url) => url.distribution_id(),
1730            Self::Directory(url) => url.distribution_id(),
1731        }
1732    }
1733
1734    fn resource_id(&self) -> ResourceId {
1735        match self {
1736            Self::Direct(url) => url.resource_id(),
1737            Self::GitDirectory(url) => url.resource_id(),
1738            Self::GitPath(url) => url.resource_id(),
1739            Self::Path(url) => url.resource_id(),
1740            Self::Directory(url) => url.resource_id(),
1741        }
1742    }
1743}
1744
1745impl Identifier for BuildableSource<'_> {
1746    fn distribution_id(&self) -> DistributionId {
1747        match self {
1748            Self::Dist(source) => source.distribution_id(),
1749            Self::Url(source) => source.distribution_id(),
1750        }
1751    }
1752
1753    fn resource_id(&self) -> ResourceId {
1754        match self {
1755            Self::Dist(source) => source.resource_id(),
1756            Self::Url(source) => source.resource_id(),
1757        }
1758    }
1759}
1760
1761#[cfg(test)]
1762mod test {
1763    use crate::{BuiltDist, Dist, RemoteSource, SourceDist, UrlString};
1764    use uv_redacted::DisplaySafeUrl;
1765
1766    /// Ensure that we don't accidentally grow the `Dist` sizes.
1767    #[test]
1768    fn dist_size() {
1769        assert!(size_of::<Dist>() <= 200, "{}", size_of::<Dist>());
1770        assert!(size_of::<BuiltDist>() <= 200, "{}", size_of::<BuiltDist>());
1771        assert!(
1772            size_of::<SourceDist>() <= 176,
1773            "{}",
1774            size_of::<SourceDist>()
1775        );
1776    }
1777
1778    #[test]
1779    fn remote_source() {
1780        for url in [
1781            "https://example.com/foo-0.1.0.tar.gz",
1782            "https://example.com/foo-0.1.0.tar.gz#fragment",
1783            "https://example.com/foo-0.1.0.tar.gz?query",
1784            "https://example.com/foo-0.1.0.tar.gz?query#fragment",
1785            "https://example.com/foo-0.1.0.tar.gz?query=1/2#fragment",
1786            "https://example.com/foo-0.1.0.tar.gz?query=1/2#fragment/3",
1787        ] {
1788            let url = DisplaySafeUrl::parse(url).unwrap();
1789            assert_eq!(url.filename().unwrap(), "foo-0.1.0.tar.gz", "{url}");
1790            let url = UrlString::from(url.clone());
1791            assert_eq!(url.filename().unwrap(), "foo-0.1.0.tar.gz", "{url}");
1792        }
1793    }
1794}